mirror of
https://gitee.com/mirrors/AllinSSL.git
synced 2026-03-07 23:31:10 +08:00
【调整】微信公众号图标调整
This commit is contained in:
2
frontend/.cursorindexingignore
Normal file
2
frontend/.cursorindexingignore
Normal file
@@ -0,0 +1,2 @@
|
||||
# Don't index SpecStory auto-save files, but allow explicit context inclusion via @ references
|
||||
.specstory/**
|
||||
1
frontend/.gitattributes
vendored
Normal file
1
frontend/.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
51
frontend/.gitignore
vendored
Normal file
51
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# Dependencies
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Testing
|
||||
coverage
|
||||
|
||||
# Turbo
|
||||
.turbo
|
||||
|
||||
# Vercel
|
||||
.vercel
|
||||
|
||||
# Build Outputs
|
||||
.next/
|
||||
out/
|
||||
build
|
||||
dist
|
||||
|
||||
|
||||
# Debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
|
||||
.turbo
|
||||
pnpm-lock.yaml
|
||||
.specstory/**、
|
||||
|
||||
.sync-log
|
||||
.sync-log/**
|
||||
|
||||
.sync-git
|
||||
.sync-git/**
|
||||
|
||||
.cursor/rules
|
||||
@@ -19,7 +19,7 @@ export interface ApiProjectType {
|
||||
// $t('t_0_1747886301644')
|
||||
export const MessagePushConfig = {
|
||||
mail: { name: $t('t_68_1745289354676'), type: 'mail' },
|
||||
wecom: { name: $t('t_33_1746773350932'), type: 'wecom' },
|
||||
workwx: { name: $t('t_33_1746773350932'), type: 'workwx' },
|
||||
dingtalk: { name: $t('t_32_1746773348993'), type: 'dingtalk' },
|
||||
feishu: { name: $t('t_34_1746773350153'), type: 'feishu' },
|
||||
webhook: { name: 'WebHook', type: 'webhook' },
|
||||
|
||||
@@ -87,14 +87,14 @@ export default defineConfig({
|
||||
// remotePath: '/www/allinssl/frontend',
|
||||
// clearRemote: true,
|
||||
// },
|
||||
{
|
||||
host: '192.168.69.167',
|
||||
port: 22,
|
||||
username: 'root',
|
||||
password: 'www.bt.cn',
|
||||
remotePath: '/www/allinssl/frontend',
|
||||
clearRemote: true,
|
||||
},
|
||||
// {
|
||||
// host: '192.168.69.167',
|
||||
// port: 22,
|
||||
// username: 'root',
|
||||
// password: 'www.bt.cn',
|
||||
// remotePath: '/www/allinssl/frontend',
|
||||
// clearRemote: true,
|
||||
// },
|
||||
]),
|
||||
// 项目同步git
|
||||
pluginProjectSyncGit({
|
||||
@@ -105,12 +105,12 @@ export default defineConfig({
|
||||
// targetDir: 'allinssl-gitlab',
|
||||
// discardChanges: true,
|
||||
// },
|
||||
// {
|
||||
// repo: 'https://github.com/allinssl/allinssl.git',
|
||||
// branch: '1.0.4',
|
||||
// targetDir: 'allinssl-github',
|
||||
// discardChanges: true,
|
||||
// },
|
||||
{
|
||||
repo: 'https://github.com/allinssl/allinssl.git',
|
||||
branch: '1.0.5',
|
||||
targetDir: 'allinssl-github',
|
||||
discardChanges: true,
|
||||
},
|
||||
],
|
||||
localSync: [
|
||||
{
|
||||
|
||||
69
frontend/plugin/vite-plugin-i18n/__tests__/adapter.test.js
Normal file
69
frontend/plugin/vite-plugin-i18n/__tests__/adapter.test.js
Normal file
@@ -0,0 +1,69 @@
|
||||
import { jest } from '@jest/globals'
|
||||
import { TranslationAdapter } from '../src/translation/adapter/index.js'
|
||||
import { AIBatchAdapter } from '../src/translation/adapter/aiBatchAdapter.js'
|
||||
import { TraditionalApiAdapter } from '../src/translation/adapter/traditionalApiAdapter.js'
|
||||
import { ZhipuAITranslator } from '../src/translation/ai/zhipuAI.js'
|
||||
import * as api1 from '../src/translation/traditional/api1.js'
|
||||
|
||||
describe('翻译适配器测试', () => {
|
||||
describe('TranslationAdapter基类', () => {
|
||||
it('不应该允许直接实例化抽象基类', () => {
|
||||
expect(() => new TranslationAdapter()).toThrow('翻译适配器:抽象类不能被直接实例化')
|
||||
})
|
||||
})
|
||||
|
||||
describe('AI批量翻译适配器', () => {
|
||||
const adapter = new AIBatchAdapter()
|
||||
const text = '你好'
|
||||
const apiKey = 'test-key'
|
||||
const languages = ['enUS', 'jaJP']
|
||||
const maxRetries = 3
|
||||
|
||||
it('应该实现translate方法', () => {
|
||||
expect(typeof adapter.translate).toBe('function')
|
||||
})
|
||||
|
||||
it('应该正确处理翻译失败和重试机制', async () => {
|
||||
const mockTranslate = jest
|
||||
.spyOn(ZhipuAITranslator.prototype, 'translate')
|
||||
.mockRejectedValueOnce(new Error('API调用失败'))
|
||||
.mockResolvedValueOnce({
|
||||
text,
|
||||
translations: {
|
||||
enUS: 'Hello',
|
||||
jaJP: 'こんにちは',
|
||||
},
|
||||
})
|
||||
|
||||
const result = await adapter.translate(text, apiKey, languages, maxRetries)
|
||||
expect(mockTranslate).toHaveBeenCalledTimes(2)
|
||||
expect(result.translations.enUS).toBe('Hello')
|
||||
expect(result.translations.jaJP).toBe('こんにちは')
|
||||
})
|
||||
})
|
||||
|
||||
describe('传统API翻译适配器', () => {
|
||||
const adapter = new TraditionalApiAdapter(api1)
|
||||
|
||||
it('应该验证API模块的有效性', () => {
|
||||
expect(() => new TraditionalApiAdapter()).toThrow('传统API适配器:无效的API模块')
|
||||
})
|
||||
|
||||
it('应该能够获取支持的语言列表', () => {
|
||||
const supportedLanguages = adapter.getSupportedLanguages()
|
||||
expect(Array.isArray(supportedLanguages)).toBe(true)
|
||||
expect(supportedLanguages.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('应该正确处理不支持的语言', async () => {
|
||||
const text = '你好'
|
||||
const apiKey = 'test-key'
|
||||
const languages = ['invalidLang']
|
||||
const maxRetries = 3
|
||||
|
||||
await expect(adapter.translate(text, apiKey, languages, maxRetries)).rejects.toThrow(
|
||||
'传统API适配器:不支持的目标语言',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
120
frontend/plugin/vite-plugin-i18n/__tests__/cache.test.js
Normal file
120
frontend/plugin/vite-plugin-i18n/__tests__/cache.test.js
Normal file
@@ -0,0 +1,120 @@
|
||||
import { jest } from '@jest/globals'
|
||||
import { promises as fs } from 'fs'
|
||||
import path from 'path'
|
||||
import { CacheManager } from '../src/cache/index.js'
|
||||
|
||||
jest.mock('fs', () => ({
|
||||
promises: {
|
||||
mkdir: jest.fn(),
|
||||
readFile: jest.fn(),
|
||||
writeFile: jest.fn(),
|
||||
access: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('CacheManager', () => {
|
||||
const cachePath = './test-cache.json'
|
||||
let cacheManager
|
||||
|
||||
beforeEach(() => {
|
||||
cacheManager = new CacheManager(cachePath)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('initCache', () => {
|
||||
it('应该正确初始化缓存', async () => {
|
||||
const mockCacheData = {
|
||||
test: {
|
||||
text: 'test',
|
||||
translations: { enUS: 'test' },
|
||||
timestamp: '2023-01-01T00:00:00.000Z',
|
||||
},
|
||||
}
|
||||
|
||||
fs.access.mockResolvedValueOnce()
|
||||
fs.readFile.mockResolvedValueOnce(JSON.stringify(mockCacheData))
|
||||
|
||||
await cacheManager.initCache()
|
||||
expect(cacheManager.cache.get('test')).toEqual(mockCacheData.test)
|
||||
})
|
||||
|
||||
it('处理缓存文件不存在的情况', async () => {
|
||||
fs.access.mockRejectedValueOnce(new Error('文件不存在'))
|
||||
|
||||
await cacheManager.initCache()
|
||||
expect(cacheManager.cache.size).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getCachedTranslations', () => {
|
||||
beforeEach(async () => {
|
||||
cacheManager.cache.set('hello', {
|
||||
text: 'hello',
|
||||
translations: {
|
||||
enUS: 'Hello',
|
||||
jaJP: 'こんにちは',
|
||||
},
|
||||
timestamp: '2023-01-01T00:00:00.000Z',
|
||||
})
|
||||
})
|
||||
|
||||
it('应该返回缓存的翻译', async () => {
|
||||
const texts = ['hello', 'world']
|
||||
const languages = ['enUS', 'jaJP']
|
||||
|
||||
const { cached, uncached } = await cacheManager.getCachedTranslations(texts, languages)
|
||||
|
||||
expect(cached.hello).toBeDefined()
|
||||
expect(uncached).toContain('world')
|
||||
})
|
||||
|
||||
it('检查缓存项是否包含所有必要的语言', async () => {
|
||||
const texts = ['hello']
|
||||
const languages = ['enUS', 'jaJP', 'zhCN']
|
||||
|
||||
const { cached, uncached } = await cacheManager.getCachedTranslations(texts, languages)
|
||||
|
||||
expect(uncached).toContain('hello')
|
||||
expect(Object.keys(cached)).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateCache', () => {
|
||||
it('应该正确更新缓存', async () => {
|
||||
const texts = ['test']
|
||||
const translations = [
|
||||
{
|
||||
text: 'test',
|
||||
translations: {
|
||||
enUS: 'Test',
|
||||
jaJP: 'テスト',
|
||||
},
|
||||
},
|
||||
]
|
||||
const languages = ['enUS', 'jaJP']
|
||||
|
||||
await cacheManager.updateCache(texts, translations, languages)
|
||||
|
||||
const cached = cacheManager.cache.get('test')
|
||||
expect(cached.translations.enUS).toBe('Test')
|
||||
expect(cached.translations.jaJP).toBe('テスト')
|
||||
expect(fs.writeFile).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('cleanCache', () => {
|
||||
it('应该删除无效的缓存项', async () => {
|
||||
cacheManager.cache.set('valid', { text: 'valid' })
|
||||
cacheManager.cache.set('invalid', { text: 'invalid' })
|
||||
|
||||
await cacheManager.cleanCache(['valid'])
|
||||
|
||||
expect(cacheManager.cache.has('valid')).toBe(true)
|
||||
expect(cacheManager.cache.has('invalid')).toBe(false)
|
||||
expect(fs.writeFile).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
133
frontend/plugin/vite-plugin-i18n/__tests__/log.test.js
Normal file
133
frontend/plugin/vite-plugin-i18n/__tests__/log.test.js
Normal file
@@ -0,0 +1,133 @@
|
||||
import { jest } from '@jest/globals'
|
||||
import { promises as fs } from 'fs'
|
||||
import path from 'path'
|
||||
import { LogManager } from '../src/logManagement/index.js'
|
||||
|
||||
jest.mock('fs', () => ({
|
||||
promises: {
|
||||
mkdir: jest.fn(),
|
||||
appendFile: jest.fn(),
|
||||
readFile: jest.fn(),
|
||||
readdir: jest.fn(),
|
||||
stat: jest.fn(),
|
||||
unlink: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('LogManager', () => {
|
||||
const options = {
|
||||
logPath: './test-logs',
|
||||
errorLogFile: 'error.log',
|
||||
infoLogFile: 'info.log',
|
||||
}
|
||||
let logManager
|
||||
|
||||
beforeEach(() => {
|
||||
logManager = new LogManager(options)
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('init', () => {
|
||||
it('应该创建日志目录', async () => {
|
||||
await logManager.init()
|
||||
expect(fs.mkdir).toHaveBeenCalledWith(options.logPath, { recursive: true })
|
||||
})
|
||||
|
||||
it('处理创建目录失败的情况', async () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation()
|
||||
fs.mkdir.mockRejectedValueOnce(new Error('创建目录失败'))
|
||||
|
||||
await logManager.init()
|
||||
expect(consoleError).toHaveBeenCalled()
|
||||
consoleError.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('logError', () => {
|
||||
it('应该写入错误日志', async () => {
|
||||
const error = new Error('测试错误')
|
||||
await logManager.logError(error)
|
||||
|
||||
expect(fs.appendFile).toHaveBeenCalledWith(
|
||||
expect.stringContaining('error.log'),
|
||||
expect.stringContaining('测试错误'),
|
||||
)
|
||||
})
|
||||
|
||||
it('处理写入错误日志失败的情况', async () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation()
|
||||
fs.appendFile.mockRejectedValueOnce(new Error('写入失败'))
|
||||
|
||||
await logManager.logError(new Error('测试错误'))
|
||||
expect(consoleError).toHaveBeenCalled()
|
||||
consoleError.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('logInfo', () => {
|
||||
it('应该写入信息日志', async () => {
|
||||
const message = '测试信息'
|
||||
await logManager.logInfo(message)
|
||||
|
||||
expect(fs.appendFile).toHaveBeenCalledWith(expect.stringContaining('info.log'), expect.stringContaining(message))
|
||||
})
|
||||
|
||||
it('处理写入信息日志失败的情况', async () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation()
|
||||
fs.appendFile.mockRejectedValueOnce(new Error('写入失败'))
|
||||
|
||||
await logManager.logInfo('测试信息')
|
||||
expect(consoleError).toHaveBeenCalled()
|
||||
consoleError.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('cleanLogs', () => {
|
||||
it('应该删除过期的日志文件', async () => {
|
||||
const now = Date.now()
|
||||
const oldDate = now - 7 * 24 * 60 * 60 * 1000 - 1000 // 7天+1秒前
|
||||
|
||||
fs.readdir.mockResolvedValueOnce(['old.log', 'new.log'])
|
||||
fs.stat.mockImplementation((path) => {
|
||||
return Promise.resolve({
|
||||
mtimeMs: path.includes('old') ? oldDate : now,
|
||||
})
|
||||
})
|
||||
|
||||
await logManager.cleanLogs(7)
|
||||
|
||||
expect(fs.unlink).toHaveBeenCalledWith(expect.stringContaining('old.log'))
|
||||
expect(fs.unlink).not.toHaveBeenCalledWith(expect.stringContaining('new.log'))
|
||||
})
|
||||
|
||||
it('处理清理日志失败的情况', async () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation()
|
||||
fs.readdir.mockRejectedValueOnce(new Error('读取目录失败'))
|
||||
|
||||
await logManager.cleanLogs(7)
|
||||
expect(consoleError).toHaveBeenCalled()
|
||||
consoleError.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getLogs', () => {
|
||||
it('应该返回指定数量的日志行', async () => {
|
||||
const logContent = '行1\n行2\n行3\n行4\n行5'
|
||||
fs.readFile.mockResolvedValueOnce(logContent)
|
||||
|
||||
const lines = await logManager.getLogs('info', 3)
|
||||
expect(lines).toHaveLength(3)
|
||||
expect(lines[2]).toBe('行5')
|
||||
})
|
||||
|
||||
it('处理读取日志失败的情况', async () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation()
|
||||
fs.readFile.mockRejectedValueOnce(new Error('读取文件失败'))
|
||||
|
||||
const lines = await logManager.getLogs('error', 5)
|
||||
expect(lines).toHaveLength(0)
|
||||
expect(consoleError).toHaveBeenCalled()
|
||||
consoleError.mockRestore()
|
||||
})
|
||||
})
|
||||
})
|
||||
100
frontend/plugin/vite-plugin-i18n/__tests__/utils.extend.test.js
Normal file
100
frontend/plugin/vite-plugin-i18n/__tests__/utils.extend.test.js
Normal file
@@ -0,0 +1,100 @@
|
||||
import { Utils } from '../src/utils/index.js'
|
||||
|
||||
describe('Utils Extended Features', () => {
|
||||
describe('chunkArray', () => {
|
||||
it('应该正确分块数组', () => {
|
||||
const array = [1, 2, 3, 4, 5, 6, 7]
|
||||
const size = 3
|
||||
const chunks = Utils.chunkArray(array, size)
|
||||
|
||||
expect(chunks).toHaveLength(3)
|
||||
expect(chunks[0]).toEqual([1, 2, 3])
|
||||
expect(chunks[1]).toEqual([4, 5, 6])
|
||||
expect(chunks[2]).toEqual([7])
|
||||
})
|
||||
|
||||
it('处理空数组', () => {
|
||||
const chunks = Utils.chunkArray([], 2)
|
||||
expect(chunks).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('delay', () => {
|
||||
it('应该延迟执行指定时间', async () => {
|
||||
const start = Date.now()
|
||||
await Utils.delay(100)
|
||||
const duration = Date.now() - start
|
||||
|
||||
expect(duration).toBeGreaterThanOrEqual(100)
|
||||
})
|
||||
})
|
||||
|
||||
describe('extractChineseTexts', () => {
|
||||
it('应该正确提取中文内容', () => {
|
||||
const content = `
|
||||
$t('你好世界')
|
||||
$t("测试文本")
|
||||
$t('Hello World')
|
||||
`
|
||||
const templateRegex = /\$t\(['"]([^'"]+)['"]\)/g
|
||||
|
||||
const texts = Utils.extractChineseTexts(content, templateRegex)
|
||||
expect(texts.size).toBe(2)
|
||||
expect(texts.has('你好世界')).toBe(true)
|
||||
expect(texts.has('测试文本')).toBe(true)
|
||||
expect(texts.has('Hello World')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('mergeTranslations', () => {
|
||||
it('应该正确合并翻译结果', () => {
|
||||
const target = {
|
||||
key1: 'old value 1',
|
||||
key2: 'old value 2',
|
||||
}
|
||||
const source = {
|
||||
key1: 'new value 1',
|
||||
key3: 'new value 3',
|
||||
}
|
||||
|
||||
const result = Utils.mergeTranslations(target, source)
|
||||
expect(result).toEqual({
|
||||
key1: 'new value 1',
|
||||
key2: 'old value 2',
|
||||
key3: 'new value 3',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('isValidLanguageCode', () => {
|
||||
it('应该验证语言代码格式', () => {
|
||||
expect(Utils.isValidLanguageCode('zhCN')).toBe(true)
|
||||
expect(Utils.isValidLanguageCode('enUS')).toBe(true)
|
||||
expect(Utils.isValidLanguageCode('zh-CN')).toBe(false)
|
||||
expect(Utils.isValidLanguageCode('123')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatError', () => {
|
||||
it('应该正确格式化错误信息', () => {
|
||||
const error = new Error('测试错误')
|
||||
const formatted = Utils.formatError(error)
|
||||
|
||||
expect(formatted).toHaveProperty('message', '测试错误')
|
||||
expect(formatted).toHaveProperty('stack')
|
||||
expect(formatted).toHaveProperty('timestamp')
|
||||
expect(new Date(formatted.timestamp)).toBeInstanceOf(Date)
|
||||
})
|
||||
})
|
||||
|
||||
describe('generateId', () => {
|
||||
it('应该生成唯一的标识符', () => {
|
||||
const id1 = Utils.generateId()
|
||||
const id2 = Utils.generateId()
|
||||
|
||||
expect(id1).toMatch(/^translation_\d+$/)
|
||||
expect(id2).toMatch(/^translation_\d+$/)
|
||||
expect(id1).not.toBe(id2)
|
||||
})
|
||||
})
|
||||
})
|
||||
62
frontend/plugin/vite-plugin-i18n/__tests__/utils.test.js
Normal file
62
frontend/plugin/vite-plugin-i18n/__tests__/utils.test.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Utils } from '../src/utils/index.js'
|
||||
|
||||
describe('Utils', () => {
|
||||
describe('isChineseText', () => {
|
||||
it('应该正确识别中文文本', () => {
|
||||
expect(Utils.isChineseText('你好')).toBe(true)
|
||||
expect(Utils.isChineseText('Hello')).toBe(false)
|
||||
expect(Utils.isChineseText('Hello 你好')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateConfig', () => {
|
||||
it('应该验证配置对象', () => {
|
||||
const validConfig = {
|
||||
apiKey: { zhipuAI: 'test-key' },
|
||||
languages: ['zhCN', 'enUS'],
|
||||
concurrency: 10,
|
||||
interval: 1000,
|
||||
}
|
||||
|
||||
const errors = Utils.validateConfig(validConfig)
|
||||
expect(errors).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('应该检测无效的配置', () => {
|
||||
const invalidConfig = {
|
||||
apiKey: 'invalid',
|
||||
languages: ['invalid'],
|
||||
concurrency: -1,
|
||||
interval: 'invalid',
|
||||
}
|
||||
|
||||
const errors = Utils.validateConfig(invalidConfig)
|
||||
expect(errors.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseLanguageCode', () => {
|
||||
it('应该正确解析语言代码', () => {
|
||||
const result = Utils.parseLanguageCode('zhCN')
|
||||
expect(result).toEqual({
|
||||
language: 'zh',
|
||||
region: 'CN',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatTranslations', () => {
|
||||
it('应该正确格式化翻译结果', () => {
|
||||
const translations = {
|
||||
hello: ' Hello World ',
|
||||
welcome: ' 欢迎 ',
|
||||
}
|
||||
|
||||
const formatted = Utils.formatTranslations(translations)
|
||||
expect(formatted).toEqual({
|
||||
hello: 'Hello World',
|
||||
welcome: '欢迎',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
112
frontend/plugin/vite-plugin-i18n/__tests__/zhipuAI.test.js
Normal file
112
frontend/plugin/vite-plugin-i18n/__tests__/zhipuAI.test.js
Normal file
@@ -0,0 +1,112 @@
|
||||
import { jest } from '@jest/globals'
|
||||
import axios from 'axios'
|
||||
import { ZhipuAITranslator } from '../src/translation/ai/zhipuAI.js'
|
||||
|
||||
jest.mock('axios')
|
||||
|
||||
describe('ZhipuAITranslator', () => {
|
||||
const apiKey = 'test-key'
|
||||
let translator
|
||||
|
||||
beforeEach(() => {
|
||||
translator = new ZhipuAITranslator(apiKey)
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('generatePrompt', () => {
|
||||
it('应该生成正确的提示词', () => {
|
||||
const text = '你好'
|
||||
const languages = ['enUS', 'jaJP']
|
||||
|
||||
const prompt = translator.generatePrompt(text, languages)
|
||||
expect(prompt).toContain('请将以下文本翻译成')
|
||||
expect(prompt).toContain('en-US, ja-JP')
|
||||
expect(prompt).toContain(text)
|
||||
})
|
||||
})
|
||||
|
||||
describe('translate', () => {
|
||||
const text = '你好'
|
||||
const languages = ['enUS', 'jaJP']
|
||||
|
||||
it('应该成功调用API并返回翻译结果', async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
content: JSON.stringify({
|
||||
'en-US': 'Hello',
|
||||
'ja-JP': 'こんにちは',
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
axios.post.mockResolvedValueOnce(mockResponse)
|
||||
|
||||
const result = await translator.translate(text, languages)
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/chatglm_turbo'),
|
||||
expect.any(Object),
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
text: '你好',
|
||||
translations: {
|
||||
enUS: 'Hello',
|
||||
jaJP: 'こんにちは',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('处理API调用失败的情况', async () => {
|
||||
axios.post.mockRejectedValueOnce(new Error('API调用失败'))
|
||||
|
||||
await expect(translator.translate(text, languages)).rejects.toThrow('智谱AI翻译失败')
|
||||
})
|
||||
|
||||
it('处理无效的API响应', async () => {
|
||||
const mockResponse = {
|
||||
data: {},
|
||||
}
|
||||
|
||||
axios.post.mockResolvedValueOnce(mockResponse)
|
||||
|
||||
await expect(translator.translate(text, languages)).rejects.toThrow('无效的API响应')
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateApiKey', () => {
|
||||
it('应该成功验证有效的API密钥', async () => {
|
||||
axios.get.mockResolvedValueOnce({})
|
||||
|
||||
const isValid = await translator.validateApiKey()
|
||||
expect(isValid).toBe(true)
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/validate'),
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('处理无效的API密钥', async () => {
|
||||
axios.get.mockRejectedValueOnce(new Error('无效的密钥'))
|
||||
|
||||
const isValid = await translator.validateApiKey()
|
||||
expect(isValid).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
91
frontend/plugin/vite-plugin-turborepo-deploy/.gitignore
vendored
Normal file
91
frontend/plugin/vite-plugin-turborepo-deploy/.gitignore
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# IDE files
|
||||
.idea
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Vite
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Vitest
|
||||
/coverage
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@baota/vite-plugin-turborepo-deploy",
|
||||
"version": "3.1.0",
|
||||
"description": "Vite plugin for automated Turborepo workspace build deployment, local file sync, and Git management.",
|
||||
"main": "dist/index.js",
|
||||
"main": "dist/index.cjs",
|
||||
"module": "dist/index.mjs",
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
@@ -15,9 +15,15 @@
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.es.js",
|
||||
"require": "./dist/index.cjs.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
"development": {
|
||||
"import": "./src/index.ts",
|
||||
"require": "./src/index.ts"
|
||||
},
|
||||
"default": {
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.cjs",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
@@ -37,17 +43,21 @@
|
||||
"node": ">=16"
|
||||
},
|
||||
"dependencies": {
|
||||
"archiver": "^7.0.1",
|
||||
"chalk": "^5.3.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"ora": "^8.0.1",
|
||||
"picomatch": "^3.0.1",
|
||||
"simple-git": "^3.22.0",
|
||||
"yauzl": "^3.2.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/archiver": "^6.0.3",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/node": "^20.11.24",
|
||||
"@types/picomatch": "^2.3.3",
|
||||
"@types/yauzl": "^2.10.3",
|
||||
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
||||
"@typescript-eslint/parser": "^7.1.0",
|
||||
"eslint": "^8.57.0",
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import type { LocalSyncConfig } from '../types';
|
||||
import type { Logger } from "./logger";
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import picomatch from 'picomatch'; // For glob matching if not using regex directly
|
||||
import os from "os";
|
||||
import { exec as execCallback } from "child_process";
|
||||
/**
|
||||
* 本地文件同步模塊 - 跨平台實現
|
||||
*
|
||||
* 使用 Node.js 原生庫實現跨平台文件壓縮和解壓功能:
|
||||
* - archiver: 跨平台壓縮庫,替代 Unix zip 命令
|
||||
* - yauzl: 跨平台解壓庫,替代 Unix unzip 命令
|
||||
* - fs-extra: 增強的文件系統操作
|
||||
*
|
||||
* 支持 Windows、Linux、macOS 等所有 Node.js 支持的平台
|
||||
*/
|
||||
|
||||
// 使用Promise包装exec函数,不依赖util.promisify
|
||||
const exec = (command: string): Promise<{ stdout: string; stderr: string }> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
execCallback(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve({ stdout, stderr });
|
||||
});
|
||||
});
|
||||
};
|
||||
import type { LocalSyncConfig } from "../types";
|
||||
import type { Logger } from "./logger";
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import picomatch from "picomatch"; // For glob matching if not using regex directly
|
||||
import os from "os";
|
||||
import archiver from "archiver";
|
||||
import yauzl from "yauzl";
|
||||
import { isSubdirectoryOf, analyzePathRelationship } from "./utils";
|
||||
|
||||
// 缓存已创建的临时压缩文件
|
||||
interface CompressionCache {
|
||||
@@ -60,66 +60,66 @@ async function createTempDir(): Promise<string> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成排除选项字符串
|
||||
* 生成排除模式數組,適配 archiver 庫的 ignore 選項
|
||||
* @param config 同步配置
|
||||
* @param sourcePath 源路径
|
||||
* @param targetPath 目标路径
|
||||
* @param tempDir 临时目录
|
||||
* @returns 排除选项字符串
|
||||
* @returns 排除模式數組
|
||||
*/
|
||||
function generateExcludeOptions(
|
||||
function generateExcludePatterns(
|
||||
config: LocalSyncConfig,
|
||||
sourcePath: string,
|
||||
targetPath: string,
|
||||
tempDir: string,
|
||||
): string {
|
||||
let excludeOptions = "";
|
||||
): string[] {
|
||||
const excludePatterns: string[] = [];
|
||||
|
||||
// 处理排除目录
|
||||
// 處理排除目錄
|
||||
if (config.excludeDirs && config.excludeDirs.length > 0) {
|
||||
const excludeDirsFormatted = config.excludeDirs
|
||||
.map((dir) => {
|
||||
// 移除通配符,获取基本目录名
|
||||
const baseDirName = dir.replace(/^\*\*\//, "");
|
||||
return `-x "*${baseDirName}*"`;
|
||||
})
|
||||
.join(" ");
|
||||
excludeOptions += ` ${excludeDirsFormatted}`;
|
||||
config.excludeDirs.forEach((dir) => {
|
||||
// 移除通配符前綴,轉換為 glob 模式
|
||||
const baseDirName = dir.replace(/^\*\*\//, "");
|
||||
excludePatterns.push(`**/${baseDirName}/**`);
|
||||
excludePatterns.push(`${baseDirName}/**`);
|
||||
});
|
||||
}
|
||||
|
||||
// 处理排除文件
|
||||
// 處理排除文件
|
||||
if (config.excludeFiles && config.excludeFiles.length > 0) {
|
||||
const excludeFilesFormatted = config.excludeFiles
|
||||
.map((file) => {
|
||||
return `-x "*${file.replace(/^\*\*\//, "")}*"`;
|
||||
})
|
||||
.join(" ");
|
||||
excludeOptions += ` ${excludeFilesFormatted}`;
|
||||
config.excludeFiles.forEach((file) => {
|
||||
const baseFileName = file.replace(/^\*\*\//, "");
|
||||
excludePatterns.push(`**/${baseFileName}`);
|
||||
excludePatterns.push(`${baseFileName}`);
|
||||
});
|
||||
}
|
||||
|
||||
// 处理正则排除
|
||||
// 處理正則排除
|
||||
if (config.exclude && config.exclude.length > 0) {
|
||||
const excludeRegexFormatted = config.exclude
|
||||
.map((pattern) => {
|
||||
return `-x "*${pattern}*"`;
|
||||
})
|
||||
.join(" ");
|
||||
excludeOptions += ` ${excludeRegexFormatted}`;
|
||||
config.exclude.forEach((pattern) => {
|
||||
// 將正則模式轉換為 glob 模式
|
||||
excludePatterns.push(`**/*${pattern}*`);
|
||||
excludePatterns.push(`*${pattern}*`);
|
||||
});
|
||||
}
|
||||
|
||||
// 始终排除目标路径,避免递归
|
||||
// 始終排除目標路徑,避免遞歸
|
||||
const relativeTargetPath = path.relative(sourcePath, targetPath);
|
||||
if (relativeTargetPath) {
|
||||
excludeOptions += ` -x "*${relativeTargetPath}*"`;
|
||||
if (relativeTargetPath && relativeTargetPath !== ".") {
|
||||
excludePatterns.push(`${relativeTargetPath}/**`);
|
||||
excludePatterns.push(`**/${relativeTargetPath}/**`);
|
||||
}
|
||||
|
||||
// 排除所有.sync-git目录
|
||||
excludeOptions += ` -x "*.sync-git*"`;
|
||||
// 排除所有 .sync-git 目錄
|
||||
excludePatterns.push("**/.sync-git/**");
|
||||
excludePatterns.push(".sync-git/**");
|
||||
|
||||
// 排除临时目录
|
||||
excludeOptions += ` -x "*${path.basename(tempDir)}*"`;
|
||||
// 排除臨時目錄
|
||||
const tempDirName = path.basename(tempDir);
|
||||
excludePatterns.push(`**/${tempDirName}/**`);
|
||||
excludePatterns.push(`${tempDirName}/**`);
|
||||
|
||||
return excludeOptions;
|
||||
return excludePatterns;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,6 +157,100 @@ function cleanExpiredCache(): void {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 處理同步錯誤,提供具體的診斷信息和解決建議
|
||||
* @param error 捕獲的錯誤
|
||||
* @param sourcePath 源路徑
|
||||
* @param targetPath 目標路徑
|
||||
* @param config 同步配置
|
||||
* @param logger 日誌記錄器
|
||||
*/
|
||||
function handleSyncError(
|
||||
error: Error,
|
||||
sourcePath: string,
|
||||
targetPath: string,
|
||||
config: LocalSyncConfig,
|
||||
logger: Logger,
|
||||
): void {
|
||||
logger.error(`❌ 同步失敗: ${sourcePath} -> ${targetPath}`);
|
||||
|
||||
// 根據錯誤類型提供具體的診斷和建議
|
||||
const errorMessage = error.message.toLowerCase();
|
||||
|
||||
if (
|
||||
errorMessage.includes("cannot copy") &&
|
||||
errorMessage.includes("subdirectory")
|
||||
) {
|
||||
logger.error(`🚨 檢測到自引用複製錯誤 - 這正是我們修復的問題!`);
|
||||
logger.error(` 錯誤詳情: ${error.message}`);
|
||||
logger.error(` 這表示路徑檢測邏輯可能仍有問題,請檢查:`);
|
||||
logger.error(` 1. 源路徑: ${sourcePath}`);
|
||||
logger.error(` 2. 目標路徑: ${targetPath}`);
|
||||
logger.error(` 3. 路徑關係檢測是否正確工作`);
|
||||
logger.error(` 💡 解決方案:`);
|
||||
logger.error(` - 確保目標路徑不是源路徑的子目錄`);
|
||||
logger.error(` - 或者使用相對路徑配置`);
|
||||
logger.error(` - 檢查 excludeDirs 配置是否包含目標目錄`);
|
||||
} else if (
|
||||
errorMessage.includes("enoent") ||
|
||||
errorMessage.includes("no such file")
|
||||
) {
|
||||
logger.error(`📁 文件或目錄不存在錯誤`);
|
||||
logger.error(` 錯誤詳情: ${error.message}`);
|
||||
logger.error(` 💡 解決方案:`);
|
||||
logger.error(` - 檢查源路徑是否存在: ${sourcePath}`);
|
||||
logger.error(` - 確保父目錄有寫入權限`);
|
||||
logger.error(` - 檢查路徑中是否包含特殊字符`);
|
||||
} else if (
|
||||
errorMessage.includes("eacces") ||
|
||||
errorMessage.includes("permission denied")
|
||||
) {
|
||||
logger.error(`🔒 權限錯誤`);
|
||||
logger.error(` 錯誤詳情: ${error.message}`);
|
||||
logger.error(` 💡 解決方案:`);
|
||||
logger.error(` - 檢查目標目錄的寫入權限`);
|
||||
logger.error(` - 確保沒有文件被其他程序占用`);
|
||||
logger.error(` - 在 Windows 上可能需要以管理員身份運行`);
|
||||
} else if (
|
||||
errorMessage.includes("enospc") ||
|
||||
errorMessage.includes("no space")
|
||||
) {
|
||||
logger.error(`💾 磁盤空間不足錯誤`);
|
||||
logger.error(` 錯誤詳情: ${error.message}`);
|
||||
logger.error(` 💡 解決方案:`);
|
||||
logger.error(` - 清理磁盤空間`);
|
||||
logger.error(` - 檢查目標磁盤的可用空間`);
|
||||
} else if (
|
||||
errorMessage.includes("emfile") ||
|
||||
errorMessage.includes("too many open files")
|
||||
) {
|
||||
logger.error(`📂 文件句柄過多錯誤`);
|
||||
logger.error(` 錯誤詳情: ${error.message}`);
|
||||
logger.error(` 💡 解決方案:`);
|
||||
logger.error(` - 增加系統文件句柄限制`);
|
||||
logger.error(` - 檢查是否有文件泄漏`);
|
||||
logger.error(` - 考慮使用 excludeDirs 減少處理的文件數量`);
|
||||
} else {
|
||||
logger.error(`❓ 未知錯誤`);
|
||||
logger.error(` 錯誤詳情: ${error.message}`);
|
||||
logger.error(` 💡 通用解決方案:`);
|
||||
logger.error(` - 檢查網絡連接(如果涉及遠程路徑)`);
|
||||
logger.error(` - 確保所有路徑都是有效的`);
|
||||
logger.error(` - 嘗試減少同步的文件數量`);
|
||||
}
|
||||
|
||||
// 提供配置建議
|
||||
logger.error(`⚙️ 當前配置信息:`);
|
||||
logger.error(` 模式: ${config.mode || "incremental"}`);
|
||||
logger.error(` 清空目標: ${config.clearTarget || false}`);
|
||||
logger.error(` 僅添加: ${config.addOnly || false}`);
|
||||
logger.error(` 排除目錄數量: ${config.excludeDirs?.length || 0}`);
|
||||
logger.error(` 排除文件數量: ${config.excludeFiles?.length || 0}`);
|
||||
|
||||
// 記錄完整的錯誤棧以便調試
|
||||
logger.verbose(`完整錯誤棧: ${error.stack}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用压缩方式处理源目录到子目录的复制,支持缓存
|
||||
* @param sourcePath 源路径
|
||||
@@ -198,23 +292,26 @@ async function syncViaCompression(
|
||||
try {
|
||||
if (needToCreateZip) {
|
||||
// 需要创建新的压缩文件
|
||||
const excludeOptions = generateExcludeOptions(
|
||||
const excludePatterns = generateExcludePatterns(
|
||||
config,
|
||||
sourcePath,
|
||||
targetPath,
|
||||
tempDir!,
|
||||
);
|
||||
|
||||
// 压缩源目录内容到临时文件
|
||||
// 使用跨平台壓縮函數
|
||||
logger.info(`压缩源目录 ${sourcePath} 到临时文件 ${tempZipFile}...`);
|
||||
const zipCmd = `cd "${sourcePath}" && zip -r "${tempZipFile}" .${excludeOptions}`;
|
||||
logger.verbose(`执行命令: ${zipCmd}`);
|
||||
await exec(zipCmd);
|
||||
await createZipWithArchiver(
|
||||
sourcePath,
|
||||
tempZipFile,
|
||||
excludePatterns,
|
||||
logger,
|
||||
);
|
||||
|
||||
// 将新创建的压缩文件加入缓存
|
||||
compressionCache[cacheKey] = {
|
||||
zipFile: tempZipFile,
|
||||
excludeOptions: excludeOptions,
|
||||
excludeOptions: excludePatterns.join(","),
|
||||
expiry: Date.now() + CACHE_TTL,
|
||||
};
|
||||
logger.verbose(
|
||||
@@ -229,11 +326,9 @@ async function syncViaCompression(
|
||||
}
|
||||
await fs.ensureDir(targetPath);
|
||||
|
||||
// 解压缩到目标目录
|
||||
// 使用跨平台解壓函數
|
||||
logger.info(`解压临时文件到目标目录 ${targetPath}...`);
|
||||
const unzipCmd = `unzip -o "${tempZipFile}" -d "${targetPath}"`;
|
||||
logger.verbose(`执行命令: ${unzipCmd}`);
|
||||
await exec(unzipCmd);
|
||||
await extractZipWithYauzl(tempZipFile, targetPath, logger);
|
||||
|
||||
logger.info(`成功通过压缩方案同步 ${sourcePath} 到 ${targetPath}`);
|
||||
} catch (error: any) {
|
||||
@@ -264,6 +359,207 @@ async function syncViaCompression(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 archiver 庫創建跨平台壓縮文件
|
||||
* @param sourcePath 源路徑
|
||||
* @param targetZipFile 目標zip文件路徑
|
||||
* @param excludePatterns 排除模式數組
|
||||
* @param logger 日誌記錄器
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
async function createZipWithArchiver(
|
||||
sourcePath: string,
|
||||
targetZipFile: string,
|
||||
excludePatterns: string[],
|
||||
logger: Logger,
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 確保目標目錄存在
|
||||
fs.ensureDirSync(path.dirname(targetZipFile));
|
||||
|
||||
// 創建輸出流
|
||||
const output = fs.createWriteStream(targetZipFile);
|
||||
|
||||
// 創建歸檔器實例,使用最高壓縮級別
|
||||
const archive = archiver("zip", {
|
||||
zlib: { level: 9 },
|
||||
});
|
||||
|
||||
// 監聽輸出流事件
|
||||
output.on("close", () => {
|
||||
logger.info(`壓縮完成,總共 ${archive.pointer()} 字節`);
|
||||
resolve();
|
||||
});
|
||||
|
||||
output.on("error", (err) => {
|
||||
logger.error(`輸出流錯誤: ${err.message}`);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
// 監聽歸檔器錯誤事件
|
||||
archive.on("error", (err) => {
|
||||
logger.error(`壓縮過程錯誤: ${err.message}`);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
// 監聽進度事件
|
||||
archive.on("progress", (progress) => {
|
||||
logger.verbose(
|
||||
`壓縮進度: 已處理 ${progress.entries.processed}/${progress.entries.total} 個條目`,
|
||||
);
|
||||
});
|
||||
|
||||
// 將歸檔器輸出管道連接到文件
|
||||
archive.pipe(output);
|
||||
|
||||
try {
|
||||
// 添加目錄及其內容,使用排除規則
|
||||
archive.glob("**/*", {
|
||||
cwd: sourcePath,
|
||||
ignore: excludePatterns,
|
||||
dot: true, // 包含隱藏文件
|
||||
});
|
||||
|
||||
logger.info(`開始壓縮 ${sourcePath} 到 ${targetZipFile}...`);
|
||||
logger.verbose(`排除模式: ${excludePatterns.join(", ")}`);
|
||||
|
||||
// 完成歸檔器
|
||||
archive.finalize();
|
||||
} catch (error: any) {
|
||||
logger.error(`壓縮設置錯誤: ${error.message}`);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 yauzl 庫創建跨平台解壓文件
|
||||
* @param zipFile 壓縮文件路徑
|
||||
* @param targetPath 目標解壓路徑
|
||||
* @param logger 日誌記錄器
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
async function extractZipWithYauzl(
|
||||
zipFile: string,
|
||||
targetPath: string,
|
||||
logger: Logger,
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 確保目標目錄存在
|
||||
fs.ensureDirSync(targetPath);
|
||||
|
||||
let extractedCount = 0;
|
||||
let totalEntries = 0;
|
||||
|
||||
// 打開 zip 文件
|
||||
yauzl.open(zipFile, { lazyEntries: true }, (err, zipfile) => {
|
||||
if (err) {
|
||||
logger.error(`無法打開壓縮文件 ${zipFile}: ${err.message}`);
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!zipfile) {
|
||||
const error = new Error("zipfile is undefined");
|
||||
logger.error(`壓縮文件對象為空: ${zipFile}`);
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
totalEntries = zipfile.entryCount;
|
||||
logger.info(
|
||||
`開始解壓 ${zipFile} 到 ${targetPath},共 ${totalEntries} 個條目`,
|
||||
);
|
||||
|
||||
// 監聽條目事件
|
||||
zipfile.on("entry", (entry) => {
|
||||
const entryPath = entry.fileName;
|
||||
const fullPath = path.join(targetPath, entryPath);
|
||||
|
||||
// 路徑安全檢查,防止目錄遍歷攻擊
|
||||
const normalizedPath = path.normalize(fullPath);
|
||||
if (!normalizedPath.startsWith(path.normalize(targetPath))) {
|
||||
logger.error(`檢測到不安全的路徑: ${entryPath}`);
|
||||
zipfile.readEntry();
|
||||
return;
|
||||
}
|
||||
|
||||
// 檢查是否為目錄
|
||||
if (entryPath.endsWith("/")) {
|
||||
// 創建目錄
|
||||
fs.ensureDirSync(fullPath);
|
||||
logger.verbose(`創建目錄: ${entryPath}`);
|
||||
extractedCount++;
|
||||
|
||||
// 繼續讀取下一個條目
|
||||
zipfile.readEntry();
|
||||
} else {
|
||||
// 提取文件
|
||||
zipfile.openReadStream(entry, (err, readStream) => {
|
||||
if (err) {
|
||||
logger.error(`無法讀取文件 ${entryPath}: ${err.message}`);
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!readStream) {
|
||||
logger.error(`讀取流為空: ${entryPath}`);
|
||||
reject(new Error(`無法創建讀取流: ${entryPath}`));
|
||||
return;
|
||||
}
|
||||
|
||||
// 確保父目錄存在
|
||||
fs.ensureDirSync(path.dirname(fullPath));
|
||||
|
||||
// 創建寫入流
|
||||
const writeStream = fs.createWriteStream(fullPath);
|
||||
|
||||
// 處理流錯誤
|
||||
readStream.on("error", (err) => {
|
||||
logger.error(`讀取流錯誤 ${entryPath}: ${err.message}`);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
writeStream.on("error", (err) => {
|
||||
logger.error(`寫入流錯誤 ${entryPath}: ${err.message}`);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
// 文件寫入完成
|
||||
writeStream.on("close", () => {
|
||||
extractedCount++;
|
||||
logger.verbose(
|
||||
`提取文件: ${entryPath} (${extractedCount}/${totalEntries})`,
|
||||
);
|
||||
|
||||
// 繼續讀取下一個條目
|
||||
zipfile.readEntry();
|
||||
});
|
||||
|
||||
// 將讀取流管道連接到寫入流
|
||||
readStream.pipe(writeStream);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 監聽結束事件
|
||||
zipfile.on("end", () => {
|
||||
logger.info(`解壓完成,共提取 ${extractedCount} 個條目`);
|
||||
resolve();
|
||||
});
|
||||
|
||||
// 監聽錯誤事件
|
||||
zipfile.on("error", (err) => {
|
||||
logger.error(`解壓過程錯誤: ${err.message}`);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
// 開始讀取第一個條目
|
||||
zipfile.readEntry();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function performLocalSync(
|
||||
configs: LocalSyncConfig[],
|
||||
workspaceRoot: string,
|
||||
@@ -298,9 +594,36 @@ export async function performLocalSync(
|
||||
const targetPath = path.resolve(workspaceRoot, target);
|
||||
|
||||
// 检查目标路径是否是源路径的子目录或相同目录
|
||||
const isSubdirectory =
|
||||
targetPath.startsWith(sourcePath + path.sep) ||
|
||||
targetPath === sourcePath;
|
||||
// 使用工具函數進行路徑比較,確保跨平台兼容性
|
||||
const pathAnalysis = analyzePathRelationship(targetPath, sourcePath);
|
||||
const isSubdirectory = isSubdirectoryOf(targetPath, sourcePath);
|
||||
|
||||
// 添加详细的路径调试日誌輸出
|
||||
logger.verbose(`路径正规化处理:`);
|
||||
logger.verbose(
|
||||
` 源路径: ${sourcePath} -> ${pathAnalysis.normalizedSource}`,
|
||||
);
|
||||
logger.verbose(
|
||||
` 目标路径: ${targetPath} -> ${pathAnalysis.normalizedTarget}`,
|
||||
);
|
||||
|
||||
logger.verbose(`子目录检测结果: ${isSubdirectory}`);
|
||||
if (isSubdirectory) {
|
||||
logger.verbose(`子目录检测详情:`);
|
||||
logger.verbose(` startsWith 检查: ${pathAnalysis.startsWithCheck}`);
|
||||
logger.verbose(` 相等检查: ${pathAnalysis.equalityCheck}`);
|
||||
logger.verbose(` 路径分隔符: '${pathAnalysis.separator}'`);
|
||||
}
|
||||
|
||||
// 配置驗證和用戶友好的錯誤處理
|
||||
await validateAndWarnPathConfiguration(
|
||||
config,
|
||||
sourcePath,
|
||||
targetPath,
|
||||
isSubdirectory,
|
||||
pathAnalysis,
|
||||
logger,
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`正在同步 ${sourcePath} 到 ${targetPath} (模式: ${config.mode || "incremental"})`,
|
||||
@@ -441,10 +764,8 @@ export async function performLocalSync(
|
||||
|
||||
logger.info(`成功同步 ${config.source} 到 ${target}`);
|
||||
} catch (error: any) {
|
||||
logger.error(
|
||||
`从 ${sourcePath} 同步到 ${targetPath} 时出错: ${error.message}`,
|
||||
error,
|
||||
);
|
||||
// 增強的錯誤處理,提供具體的診斷信息
|
||||
handleSyncError(error, sourcePath, targetPath, config, logger);
|
||||
// 软错误:继续执行其他任务
|
||||
}
|
||||
}
|
||||
@@ -452,6 +773,77 @@ export async function performLocalSync(
|
||||
logger.info("本地文件同步完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 驗證路徑配置並提供用戶友好的警告和建議
|
||||
* @param config 同步配置
|
||||
* @param sourcePath 源路徑
|
||||
* @param targetPath 目標路徑
|
||||
* @param isSubdirectory 是否為子目錄
|
||||
* @param pathAnalysis 路徑分析結果
|
||||
* @param logger 日誌記錄器
|
||||
*/
|
||||
async function validateAndWarnPathConfiguration(
|
||||
config: LocalSyncConfig,
|
||||
sourcePath: string,
|
||||
targetPath: string,
|
||||
isSubdirectory: boolean,
|
||||
pathAnalysis: ReturnType<typeof analyzePathRelationship>,
|
||||
logger: Logger,
|
||||
): Promise<void> {
|
||||
// 檢查相同路徑的情況
|
||||
if (pathAnalysis.equalityCheck) {
|
||||
logger.warn(`⚠️ 源路徑和目標路徑相同: ${sourcePath}`);
|
||||
logger.warn(` 這可能表示配置錯誤,請檢查您的 localSync 配置`);
|
||||
logger.warn(` 建議:修改 target 路徑以避免自我複製`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 檢查子目錄情況的配置建議
|
||||
if (isSubdirectory) {
|
||||
logger.info(`🔍 檢測到目標路徑是源路徑的子目錄,將使用壓縮方案`);
|
||||
|
||||
// 針對不同模式提供建議
|
||||
if (config.mode === "mirror") {
|
||||
logger.warn(`⚠️ 鏡像模式 + 子目錄配置可能導致不必要的複雜性`);
|
||||
logger.warn(` 建議:考慮使用 'copy' 或 'incremental' 模式`);
|
||||
}
|
||||
|
||||
// 檢查是否缺少必要的排除配置
|
||||
if (!config.excludeDirs || config.excludeDirs.length === 0) {
|
||||
logger.warn(`⚠️ 子目錄同步時建議配置 excludeDirs 以避免無限遞歸`);
|
||||
logger.warn(
|
||||
` 建議:添加 excludeDirs: ['.sync-git', 'node_modules', '.git']`,
|
||||
);
|
||||
}
|
||||
|
||||
// 特別警告常見的錯誤模式
|
||||
const relativePath = path.relative(sourcePath, targetPath);
|
||||
if (relativePath.includes(".sync-git")) {
|
||||
logger.info(`✅ 檢測到目標在 .sync-git 目錄中,這是推薦的配置`);
|
||||
} else {
|
||||
logger.warn(`⚠️ 目標路徑不在 .sync-git 目錄中: ${relativePath}`);
|
||||
logger.warn(` 建議:將目標設置為 '.sync-git/your-target' 以保持組織性`);
|
||||
}
|
||||
}
|
||||
|
||||
// 檢查路徑格式問題
|
||||
if (sourcePath.includes("\\") && targetPath.includes("/")) {
|
||||
logger.warn(`⚠️ 檢測到混合路徑分隔符,已自動正規化處理`);
|
||||
logger.info(` 原始: 源='${sourcePath}' 目標='${targetPath}'`);
|
||||
logger.info(
|
||||
` 正規化: 源='${pathAnalysis.normalizedSource}' 目標='${pathAnalysis.normalizedTarget}'`,
|
||||
);
|
||||
}
|
||||
|
||||
// 檢查潛在的性能問題
|
||||
if (config.source === "/" && !config.excludeDirs?.includes("node_modules")) {
|
||||
logger.warn(`⚠️ 從根目錄 '/' 同步時強烈建議排除 node_modules`);
|
||||
logger.warn(
|
||||
` 建議:添加 excludeDirs: ['node_modules', '.git', 'dist', 'build']`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归获取目录中的所有文件路径
|
||||
* @param dir 要扫描的目录
|
||||
@@ -473,4 +865,4 @@ async function getAllFiles(dir: string): Promise<string[]> {
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,4 +127,69 @@ export async function safeRemoveFile(filePath: string): Promise<void> {
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 正規化多個路徑,解決跨平台兼容性問題
|
||||
* @param paths 需要正規化的路徑數組
|
||||
* @returns 正規化後的路徑數組
|
||||
*/
|
||||
export function normalizePaths(...paths: string[]): string[] {
|
||||
return paths.map((p) => path.normalize(p));
|
||||
}
|
||||
|
||||
/**
|
||||
* 檢查目標路徑是否是源路徑的子目錄或相同目錄
|
||||
* 使用路徑正規化處理,確保跨平台兼容性
|
||||
* @param targetPath 目標路徑
|
||||
* @param sourcePath 源路徑
|
||||
* @returns 如果目標路徑是源路徑的子目錄或相同目錄則返回 true
|
||||
*/
|
||||
export function isSubdirectoryOf(
|
||||
targetPath: string,
|
||||
sourcePath: string,
|
||||
): boolean {
|
||||
// 正規化路徑以確保跨平台兼容性
|
||||
const normalizedTarget = path.normalize(targetPath);
|
||||
const normalizedSource = path.normalize(sourcePath);
|
||||
|
||||
// 檢查是否為子目錄或相同目錄
|
||||
return (
|
||||
normalizedTarget.startsWith(normalizedSource + path.sep) ||
|
||||
normalizedTarget === normalizedSource
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 檢查路徑關係的詳細信息,便於調試
|
||||
* @param targetPath 目標路徑
|
||||
* @param sourcePath 源路徑
|
||||
* @returns 包含檢查詳情的對象
|
||||
*/
|
||||
export function analyzePathRelationship(
|
||||
targetPath: string,
|
||||
sourcePath: string,
|
||||
): {
|
||||
isSubdirectory: boolean;
|
||||
normalizedTarget: string;
|
||||
normalizedSource: string;
|
||||
startsWithCheck: boolean;
|
||||
equalityCheck: boolean;
|
||||
separator: string;
|
||||
} {
|
||||
const normalizedTarget = path.normalize(targetPath);
|
||||
const normalizedSource = path.normalize(sourcePath);
|
||||
const startsWithCheck = normalizedTarget.startsWith(
|
||||
normalizedSource + path.sep,
|
||||
);
|
||||
const equalityCheck = normalizedTarget === normalizedSource;
|
||||
|
||||
return {
|
||||
isSubdirectory: startsWithCheck || equalityCheck,
|
||||
normalizedTarget,
|
||||
normalizedSource,
|
||||
startsWithCheck,
|
||||
equalityCheck,
|
||||
separator: path.sep,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { performAutoCommit } from "../../src/core/autoCommitHandler";
|
||||
import path from "path";
|
||||
import fs from "fs-extra";
|
||||
|
||||
// 模拟依赖
|
||||
vi.mock("simple-git", () => {
|
||||
// 创建简单的模拟实现
|
||||
const mockGit = {
|
||||
checkIsRepo: vi.fn().mockResolvedValue(true),
|
||||
branchLocal: vi.fn().mockResolvedValue({ current: "develop" }),
|
||||
log: vi.fn().mockResolvedValue({
|
||||
all: [
|
||||
{ hash: "1234567", message: "一般提交1" },
|
||||
{ hash: "2345678", message: "一般提交2" },
|
||||
{ hash: "3456789", message: "/** 提交分隔符 **/" },
|
||||
{ hash: "4567890", message: "之前的提交" },
|
||||
],
|
||||
}),
|
||||
status: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ isClean: vi.fn().mockReturnValue(true) }),
|
||||
add: vi.fn().mockResolvedValue(undefined),
|
||||
commit: vi.fn().mockResolvedValue(undefined),
|
||||
push: vi.fn().mockResolvedValue(undefined),
|
||||
checkout: vi.fn().mockResolvedValue(undefined),
|
||||
reset: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
return {
|
||||
default: vi.fn().mockReturnValue(mockGit),
|
||||
__esModule: true,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("fs-extra", () => ({
|
||||
existsSync: vi.fn().mockReturnValue(true),
|
||||
__esModule: true,
|
||||
}));
|
||||
|
||||
// 模拟日志记录器
|
||||
const mockLogger = {
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
info: vi.fn(),
|
||||
verbose: vi.fn(),
|
||||
setLogLevel: vi.fn(),
|
||||
};
|
||||
|
||||
describe("performAutoCommit", () => {
|
||||
const mockViteRoot = "/test/root";
|
||||
const mockSharedCommitMessagesHolder = { current: null as string[] | null };
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("应该处理单个项目的自动提交 (独立模块)", async () => {
|
||||
const config = {
|
||||
projects: [
|
||||
{
|
||||
targetDir: "services/api-gateway",
|
||||
projectName: "api-gateway",
|
||||
watchAuthor: "张三",
|
||||
push: true,
|
||||
},
|
||||
],
|
||||
enableSharedCommits: true,
|
||||
insertSeparator: true,
|
||||
};
|
||||
|
||||
await performAutoCommit(
|
||||
config,
|
||||
mockViteRoot,
|
||||
mockLogger,
|
||||
mockSharedCommitMessagesHolder,
|
||||
);
|
||||
|
||||
// 验证日志和基本操作
|
||||
expect(mockLogger.info).toHaveBeenCalledWith("开始自动提交操作...");
|
||||
expect(mockLogger.info).toHaveBeenCalledWith("已重置共享提交信息缓冲区");
|
||||
expect(mockLogger.info).toHaveBeenCalledWith(
|
||||
"处理自动提交项目: api-gateway",
|
||||
);
|
||||
expect(mockLogger.info).toHaveBeenCalledWith("自动提交操作完成");
|
||||
|
||||
// 验证Git操作
|
||||
const simpleGit = require("simple-git").default;
|
||||
const mockGit = simpleGit();
|
||||
expect(mockGit.log).toHaveBeenCalled();
|
||||
expect(mockGit.commit).toHaveBeenCalledTimes(2); // 一次正常提交,一次分隔符
|
||||
expect(mockGit.push).toHaveBeenCalledTimes(2); // 一次正常推送,一次分隔符推送
|
||||
});
|
||||
|
||||
it("应该在禁用共享提交时不重置共享缓冲区", async () => {
|
||||
const config = {
|
||||
projects: [
|
||||
{
|
||||
targetDir: "services/api-gateway",
|
||||
projectName: "api-gateway",
|
||||
watchAuthor: "张三",
|
||||
},
|
||||
],
|
||||
enableSharedCommits: false,
|
||||
insertSeparator: true,
|
||||
};
|
||||
|
||||
await performAutoCommit(
|
||||
config,
|
||||
mockViteRoot,
|
||||
mockLogger,
|
||||
mockSharedCommitMessagesHolder,
|
||||
);
|
||||
|
||||
// 验证没有重置共享缓冲区
|
||||
expect(mockLogger.info).not.toHaveBeenCalledWith(
|
||||
"已重置共享提交信息缓冲区",
|
||||
);
|
||||
});
|
||||
|
||||
it("应该在项目不存在时跳过处理", async () => {
|
||||
// 模拟文件不存在
|
||||
vi.mocked(fs.existsSync).mockReturnValueOnce(false);
|
||||
|
||||
const config = {
|
||||
projects: [
|
||||
{
|
||||
targetDir: "services/non-existent",
|
||||
projectName: "non-existent",
|
||||
watchAuthor: "张三",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await performAutoCommit(
|
||||
config,
|
||||
mockViteRoot,
|
||||
mockLogger,
|
||||
mockSharedCommitMessagesHolder,
|
||||
);
|
||||
|
||||
expect(mockLogger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("不存在,跳过此项目"),
|
||||
);
|
||||
// 验证没有进行Git操作
|
||||
const simpleGit = require("simple-git").default;
|
||||
expect(simpleGit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("应该使用共享提交信息", async () => {
|
||||
// 设置共享提交信息
|
||||
mockSharedCommitMessagesHolder.current = ["[abcdef1] 共享的提交信息"];
|
||||
|
||||
const config = {
|
||||
projects: [
|
||||
{
|
||||
targetDir: "services/consumer",
|
||||
projectName: "consumer",
|
||||
useSharedCommits: true,
|
||||
push: true,
|
||||
},
|
||||
],
|
||||
enableSharedCommits: true,
|
||||
};
|
||||
|
||||
await performAutoCommit(
|
||||
config,
|
||||
mockViteRoot,
|
||||
mockLogger,
|
||||
mockSharedCommitMessagesHolder,
|
||||
);
|
||||
|
||||
// 验证使用了共享提交信息
|
||||
expect(mockLogger.info).toHaveBeenCalledWith("[consumer] 使用共享提交信息");
|
||||
|
||||
// 验证提交操作
|
||||
const simpleGit = require("simple-git").default;
|
||||
const mockGit = simpleGit();
|
||||
expect(mockGit.commit).toHaveBeenCalled();
|
||||
expect(mockGit.push).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("应该在没有提交时跳过", async () => {
|
||||
// 模拟没有提交记录
|
||||
const simpleGit = require("simple-git").default;
|
||||
const mockGit = simpleGit();
|
||||
vi.mocked(mockGit.log).mockResolvedValueOnce({ all: [] });
|
||||
|
||||
const config = {
|
||||
projects: [
|
||||
{
|
||||
targetDir: "services/no-commits",
|
||||
projectName: "no-commits",
|
||||
watchAuthor: "张三",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await performAutoCommit(
|
||||
config,
|
||||
mockViteRoot,
|
||||
mockLogger,
|
||||
mockSharedCommitMessagesHolder,
|
||||
);
|
||||
|
||||
// 验证没有进行提交操作
|
||||
expect(mockGit.commit).not.toHaveBeenCalled();
|
||||
expect(mockLogger.info).toHaveBeenCalledWith(
|
||||
"[no-commits] 没有要处理的新提交",
|
||||
);
|
||||
});
|
||||
|
||||
it("应该处理分支切换", async () => {
|
||||
const config = {
|
||||
projects: [
|
||||
{
|
||||
targetDir: "services/branch-test",
|
||||
projectName: "branch-test",
|
||||
watchAuthor: "张三",
|
||||
branch: "feature/test",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// 模拟当前分支不是目标分支
|
||||
const simpleGit = require("simple-git").default;
|
||||
const mockGit = simpleGit();
|
||||
vi.mocked(mockGit.branchLocal).mockResolvedValueOnce({ current: "main" });
|
||||
|
||||
await performAutoCommit(
|
||||
config,
|
||||
mockViteRoot,
|
||||
mockLogger,
|
||||
mockSharedCommitMessagesHolder,
|
||||
);
|
||||
|
||||
// 验证分支切换
|
||||
expect(mockGit.checkout).toHaveBeenCalledWith("feature/test");
|
||||
expect(mockLogger.info).toHaveBeenCalledWith("切换到分支 feature/test...");
|
||||
});
|
||||
|
||||
it("应该支持多项目并发处理", async () => {
|
||||
const config = {
|
||||
projects: [
|
||||
{
|
||||
targetDir: "services/api-gateway",
|
||||
projectName: "api-gateway",
|
||||
watchAuthor: "张三",
|
||||
push: true,
|
||||
},
|
||||
{
|
||||
targetDir: "services/user-service",
|
||||
projectName: "user-service",
|
||||
useSharedCommits: true,
|
||||
push: true,
|
||||
},
|
||||
],
|
||||
enableSharedCommits: true,
|
||||
};
|
||||
|
||||
await performAutoCommit(
|
||||
config,
|
||||
mockViteRoot,
|
||||
mockLogger,
|
||||
mockSharedCommitMessagesHolder,
|
||||
);
|
||||
|
||||
// 验证处理了两个项目
|
||||
expect(mockLogger.info).toHaveBeenCalledWith(
|
||||
"处理自动提交项目: api-gateway",
|
||||
);
|
||||
expect(mockLogger.info).toHaveBeenCalledWith(
|
||||
"处理自动提交项目: user-service",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,211 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { loadConfig, processTasks } from "../../src/core/config";
|
||||
import path from "path";
|
||||
|
||||
// 模拟依赖
|
||||
vi.mock("../../src/core/logger", () => ({
|
||||
createLogger: vi.fn(() => ({
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
info: vi.fn(),
|
||||
verbose: vi.fn(),
|
||||
setLogLevel: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("../../src/core/localSync", () => ({
|
||||
performLocalSync: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock("../../src/core/gitHandler", () => ({
|
||||
updateGitProjects: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
// 导入模拟的模块
|
||||
import { performLocalSync } from "../../src/core/localSync";
|
||||
import { updateGitProjects } from "../../src/core/gitHandler";
|
||||
|
||||
describe("loadConfig", () => {
|
||||
it("应该返回空对象当没有提供选项时", () => {
|
||||
const config = loadConfig(undefined, "/test/root");
|
||||
expect(config).toEqual({});
|
||||
});
|
||||
|
||||
it("应该验证并返回有效的配置", () => {
|
||||
const options = {
|
||||
localSync: [{ source: "src", target: "dist" }],
|
||||
gitProjects: [
|
||||
{
|
||||
repo: "https://github.com/example/repo.git",
|
||||
branch: "main",
|
||||
targetDir: "services/repo",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const config = loadConfig(options, "/test/root");
|
||||
expect(config).toEqual(options);
|
||||
});
|
||||
|
||||
it("应该在无效配置时抛出错误", () => {
|
||||
const invalidOptions = {
|
||||
localSync: [
|
||||
{ source: "", target: "dist" }, // 无效的source
|
||||
],
|
||||
};
|
||||
|
||||
expect(() => loadConfig(invalidOptions, "/test/root")).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("processTasks", () => {
|
||||
const mockLogger = {
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
info: vi.fn(),
|
||||
verbose: vi.fn(),
|
||||
setLogLevel: vi.fn(),
|
||||
};
|
||||
|
||||
const mockViteConfig = {
|
||||
root: "/test/root",
|
||||
} as any;
|
||||
|
||||
const mockSharedCommitMessagesHolder = { current: null as string[] | null };
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("应该按顺序执行配置的任务", async () => {
|
||||
const config = {
|
||||
localSync: [{ source: "src", target: "dist" }],
|
||||
gitProjects: [
|
||||
{
|
||||
repo: "https://github.com/example/repo.git",
|
||||
branch: "main",
|
||||
targetDir: "services/repo",
|
||||
},
|
||||
],
|
||||
taskOrder: ["localSync", "updateGitProjects"],
|
||||
};
|
||||
|
||||
await processTasks(
|
||||
config,
|
||||
mockViteConfig,
|
||||
mockLogger,
|
||||
mockSharedCommitMessagesHolder,
|
||||
);
|
||||
|
||||
expect(mockLogger.info).toHaveBeenCalledWith(
|
||||
"执行任务顺序: localSync, updateGitProjects",
|
||||
);
|
||||
expect(mockLogger.info).toHaveBeenCalledWith("开始任务: localSync");
|
||||
expect(performLocalSync).toHaveBeenCalledWith(
|
||||
config.localSync,
|
||||
mockViteConfig.root,
|
||||
mockLogger,
|
||||
);
|
||||
expect(mockLogger.info).toHaveBeenCalledWith("任务 localSync 完成。");
|
||||
|
||||
expect(mockLogger.info).toHaveBeenCalledWith("开始任务: updateGitProjects");
|
||||
expect(updateGitProjects).toHaveBeenCalledWith(
|
||||
config.gitProjects,
|
||||
mockViteConfig.root,
|
||||
mockLogger,
|
||||
mockSharedCommitMessagesHolder,
|
||||
);
|
||||
expect(mockLogger.info).toHaveBeenCalledWith(
|
||||
"任务 updateGitProjects 完成。",
|
||||
);
|
||||
});
|
||||
|
||||
it("应该使用默认任务顺序当未指定时", async () => {
|
||||
const config = {
|
||||
localSync: [{ source: "src", target: "dist" }],
|
||||
gitProjects: [
|
||||
{
|
||||
repo: "https://github.com/example/repo.git",
|
||||
branch: "main",
|
||||
targetDir: "services/repo",
|
||||
},
|
||||
],
|
||||
// 未指定taskOrder
|
||||
};
|
||||
|
||||
await processTasks(
|
||||
config,
|
||||
mockViteConfig,
|
||||
mockLogger,
|
||||
mockSharedCommitMessagesHolder,
|
||||
);
|
||||
|
||||
// 默认顺序: localSync, updateGitProjects
|
||||
expect(mockLogger.info).toHaveBeenCalledWith(
|
||||
"执行任务顺序: localSync, updateGitProjects",
|
||||
);
|
||||
expect(performLocalSync).toHaveBeenCalled();
|
||||
expect(updateGitProjects).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("应该处理任务错误并继续执行", async () => {
|
||||
const config = {
|
||||
localSync: [{ source: "src", target: "dist" }],
|
||||
gitProjects: [
|
||||
{
|
||||
repo: "https://github.com/example/repo.git",
|
||||
branch: "main",
|
||||
targetDir: "services/repo",
|
||||
},
|
||||
],
|
||||
taskOrder: ["localSync", "updateGitProjects"],
|
||||
};
|
||||
|
||||
const error = new Error("测试错误");
|
||||
(performLocalSync as any).mockRejectedValueOnce(error);
|
||||
|
||||
await processTasks(
|
||||
config,
|
||||
mockViteConfig,
|
||||
mockLogger,
|
||||
mockSharedCommitMessagesHolder,
|
||||
);
|
||||
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
"任务 localSync 执行错误: 测试错误",
|
||||
error,
|
||||
);
|
||||
expect(updateGitProjects).toHaveBeenCalled(); // 第二个任务仍然执行
|
||||
});
|
||||
|
||||
it("应该在关键错误时中断流程", async () => {
|
||||
const config = {
|
||||
localSync: [{ source: "src", target: "dist" }],
|
||||
gitProjects: [
|
||||
{
|
||||
repo: "https://github.com/example/repo.git",
|
||||
branch: "main",
|
||||
targetDir: "services/repo",
|
||||
},
|
||||
],
|
||||
taskOrder: ["localSync", "updateGitProjects"],
|
||||
};
|
||||
|
||||
const criticalError = new Error("关键错误") as Error & {
|
||||
isCritical: boolean;
|
||||
};
|
||||
criticalError.isCritical = true;
|
||||
(performLocalSync as any).mockRejectedValueOnce(criticalError);
|
||||
|
||||
await expect(
|
||||
processTasks(
|
||||
config,
|
||||
mockViteConfig,
|
||||
mockLogger,
|
||||
mockSharedCommitMessagesHolder,
|
||||
),
|
||||
).rejects.toThrow("关键错误");
|
||||
|
||||
expect(updateGitProjects).not.toHaveBeenCalled(); // 第二个任务不应执行
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,270 @@
|
||||
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
||||
import path from "path";
|
||||
import fs from "fs-extra";
|
||||
import {
|
||||
resolvePath,
|
||||
createError,
|
||||
formatDate,
|
||||
isAbsolutePath,
|
||||
safeRemoveFile,
|
||||
isSubdirectoryOf,
|
||||
normalizePaths,
|
||||
analyzePathRelationship,
|
||||
} from "../../src/core/utils";
|
||||
|
||||
// 创建临时测试文件的路径
|
||||
const tmpDir = path.join(process.cwd(), "tests", "tmp");
|
||||
const tmpFile = path.join(tmpDir, "test-file.txt");
|
||||
|
||||
// 确保临时目录存在
|
||||
beforeAll(async () => {
|
||||
await fs.ensureDir(tmpDir);
|
||||
});
|
||||
|
||||
// 测试后清理
|
||||
afterAll(async () => {
|
||||
await fs.remove(tmpDir);
|
||||
});
|
||||
|
||||
describe("resolvePath", () => {
|
||||
it("应该正确解析相对路径", () => {
|
||||
const viteRoot = "/test/root";
|
||||
const relativePath = "src/components";
|
||||
const expected = path.resolve(viteRoot, relativePath);
|
||||
|
||||
expect(resolvePath(viteRoot, relativePath)).toBe(expected);
|
||||
});
|
||||
|
||||
it("应该保留绝对路径", () => {
|
||||
const viteRoot = "/test/root";
|
||||
const absolutePath = "/absolute/path";
|
||||
|
||||
expect(resolvePath(viteRoot, absolutePath)).toBe(absolutePath);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createError", () => {
|
||||
it("应该创建非关键错误", () => {
|
||||
const message = "Test error";
|
||||
const error = createError(message);
|
||||
|
||||
expect(error.message).toBe(message);
|
||||
expect(error.isCritical).toBe(false);
|
||||
expect(error.timestamp).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it("应该创建关键错误", () => {
|
||||
const message = "Critical error";
|
||||
const error = createError(message, true);
|
||||
|
||||
expect(error.message).toBe(message);
|
||||
expect(error.isCritical).toBe(true);
|
||||
expect(error.timestamp).toBeInstanceOf(Date);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatDate", () => {
|
||||
it("应该正确格式化日期", () => {
|
||||
const date = new Date(2023, 0, 1); // 2023-01-01
|
||||
expect(formatDate(date)).toBe("2023-01-01");
|
||||
|
||||
const date2 = new Date(2023, 11, 31); // 2023-12-31
|
||||
expect(formatDate(date2)).toBe("2023-12-31");
|
||||
});
|
||||
|
||||
it("应该在月份和日期前补零", () => {
|
||||
const date = new Date(2023, 0, 1); // 2023-01-01
|
||||
expect(formatDate(date)).toBe("2023-01-01");
|
||||
|
||||
const date2 = new Date(2023, 8, 9); // 2023-09-09
|
||||
expect(formatDate(date2)).toBe("2023-09-09");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isAbsolutePath", () => {
|
||||
it("应该识别绝对路径", () => {
|
||||
const absolutePath = path.resolve("/absolute/path");
|
||||
expect(isAbsolutePath(absolutePath)).toBe(true);
|
||||
});
|
||||
|
||||
it("应该识别相对路径", () => {
|
||||
expect(isAbsolutePath("relative/path")).toBe(false);
|
||||
expect(isAbsolutePath("./relative/path")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("safeRemoveFile", () => {
|
||||
it("应该删除存在的文件", async () => {
|
||||
// 创建测试文件
|
||||
await fs.writeFile(tmpFile, "test content");
|
||||
expect(fs.existsSync(tmpFile)).toBe(true);
|
||||
|
||||
// 删除文件
|
||||
await safeRemoveFile(tmpFile);
|
||||
expect(fs.existsSync(tmpFile)).toBe(false);
|
||||
});
|
||||
|
||||
it("应该安全地处理不存在的文件", async () => {
|
||||
const nonExistentFile = path.join(tmpDir, "non-existent.txt");
|
||||
|
||||
// 确保文件不存在
|
||||
if (fs.existsSync(nonExistentFile)) {
|
||||
await fs.remove(nonExistentFile);
|
||||
}
|
||||
|
||||
// 不应抛出错误
|
||||
await expect(safeRemoveFile(nonExistentFile)).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizePaths", () => {
|
||||
it("应该正规化单个路径", () => {
|
||||
const paths = normalizePaths("C:/test\\path//file.txt");
|
||||
expect(paths).toHaveLength(1);
|
||||
expect(paths[0]).toBe(path.normalize("C:/test\\path//file.txt"));
|
||||
});
|
||||
|
||||
it("应该正规化多个路径", () => {
|
||||
const input = ["C:/test\\path1", "D:\\test/path2", "./relative\\path"];
|
||||
const result = normalizePaths(...input);
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0]).toBe(path.normalize("C:/test\\path1"));
|
||||
expect(result[1]).toBe(path.normalize("D:\\test/path2"));
|
||||
expect(result[2]).toBe(path.normalize("./relative\\path"));
|
||||
});
|
||||
|
||||
it("应该处理空路径数组", () => {
|
||||
const result = normalizePaths();
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isSubdirectoryOf", () => {
|
||||
describe("Unix/Linux 路径格式", () => {
|
||||
it("应该识别子目录", () => {
|
||||
expect(isSubdirectoryOf("/root/sub", "/root")).toBe(true);
|
||||
expect(isSubdirectoryOf("/root/sub/deep", "/root")).toBe(true);
|
||||
expect(isSubdirectoryOf("/root/sub/deep/file.txt", "/root")).toBe(true);
|
||||
});
|
||||
|
||||
it("应该识别相同目录", () => {
|
||||
expect(isSubdirectoryOf("/root", "/root")).toBe(true);
|
||||
expect(isSubdirectoryOf("/root/", "/root")).toBe(true);
|
||||
expect(isSubdirectoryOf("/root", "/root/")).toBe(true);
|
||||
});
|
||||
|
||||
it("应该拒绝非子目录", () => {
|
||||
expect(isSubdirectoryOf("/other", "/root")).toBe(false);
|
||||
expect(isSubdirectoryOf("/root-similar", "/root")).toBe(false);
|
||||
expect(isSubdirectoryOf("/", "/root")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Windows 路径格式", () => {
|
||||
it("应该识别子目录", () => {
|
||||
expect(isSubdirectoryOf("C:\\root\\sub", "C:\\root")).toBe(true);
|
||||
expect(isSubdirectoryOf("C:\\root\\sub\\deep", "C:\\root")).toBe(true);
|
||||
});
|
||||
|
||||
it("应该识别相同目录", () => {
|
||||
expect(isSubdirectoryOf("C:\\root", "C:\\root")).toBe(true);
|
||||
expect(isSubdirectoryOf("C:\\root\\", "C:\\root")).toBe(true);
|
||||
});
|
||||
|
||||
it("应该拒绝非子目录", () => {
|
||||
expect(isSubdirectoryOf("D:\\root", "C:\\root")).toBe(false);
|
||||
expect(isSubdirectoryOf("C:\\other", "C:\\root")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("混合路径分隔符處理", () => {
|
||||
it("应该处理混合的路径分隔符", () => {
|
||||
// 這是實際錯誤場景:正規化應該統一分隔符
|
||||
expect(isSubdirectoryOf("C:/root\\.sync-git\\target", "C:\\root")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(isSubdirectoryOf("C:\\root/.sync-git/target", "C:/root")).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("应该处理尾随分隔符", () => {
|
||||
expect(isSubdirectoryOf("C:/root/sub/", "C:/root/")).toBe(true);
|
||||
expect(isSubdirectoryOf("C:\\root\\sub\\", "C:\\root\\")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("邊界條件測試", () => {
|
||||
it("应该处理根目录", () => {
|
||||
if (process.platform === "win32") {
|
||||
expect(isSubdirectoryOf("C:\\", "C:\\")).toBe(true);
|
||||
expect(isSubdirectoryOf("C:\\sub", "C:\\")).toBe(true);
|
||||
} else {
|
||||
expect(isSubdirectoryOf("/", "/")).toBe(true);
|
||||
expect(isSubdirectoryOf("/sub", "/")).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("应该处理相对路径", () => {
|
||||
expect(isSubdirectoryOf("./sub", ".")).toBe(true);
|
||||
expect(isSubdirectoryOf("../other", ".")).toBe(false);
|
||||
expect(isSubdirectoryOf("sub/deep", "sub")).toBe(true);
|
||||
});
|
||||
|
||||
it("应该处理空字符串和特殊字符", () => {
|
||||
expect(isSubdirectoryOf("", "")).toBe(true);
|
||||
expect(isSubdirectoryOf("a", "")).toBe(false);
|
||||
expect(isSubdirectoryOf("", "a")).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("analyzePathRelationship", () => {
|
||||
it("应该提供详细的路径分析", () => {
|
||||
const targetPath = "C:/root\\.sync-git\\target";
|
||||
const sourcePath = "C:\\root";
|
||||
|
||||
const analysis = analyzePathRelationship(targetPath, sourcePath);
|
||||
|
||||
expect(analysis.isSubdirectory).toBe(true);
|
||||
expect(analysis.normalizedTarget).toBe(path.normalize(targetPath));
|
||||
expect(analysis.normalizedSource).toBe(path.normalize(sourcePath));
|
||||
expect(analysis.startsWithCheck).toBe(true);
|
||||
expect(analysis.equalityCheck).toBe(false);
|
||||
expect(analysis.separator).toBe(path.sep);
|
||||
});
|
||||
|
||||
it("应该分析相同路径", () => {
|
||||
const targetPath = "C:\\root";
|
||||
const sourcePath = "C:/root";
|
||||
|
||||
const analysis = analyzePathRelationship(targetPath, sourcePath);
|
||||
|
||||
expect(analysis.isSubdirectory).toBe(true);
|
||||
expect(analysis.startsWithCheck).toBe(false);
|
||||
expect(analysis.equalityCheck).toBe(true);
|
||||
});
|
||||
|
||||
it("应该分析非子目录关系", () => {
|
||||
const targetPath = "/other/path";
|
||||
const sourcePath = "/root";
|
||||
|
||||
const analysis = analyzePathRelationship(targetPath, sourcePath);
|
||||
|
||||
expect(analysis.isSubdirectory).toBe(false);
|
||||
expect(analysis.startsWithCheck).toBe(false);
|
||||
expect(analysis.equalityCheck).toBe(false);
|
||||
});
|
||||
|
||||
it("应该包含正確的分隔符信息", () => {
|
||||
const analysis = analyzePathRelationship("any", "path");
|
||||
|
||||
expect(analysis.separator).toBe(path.sep);
|
||||
if (process.platform === "win32") {
|
||||
expect(analysis.separator).toBe("\\");
|
||||
} else {
|
||||
expect(analysis.separator).toBe("/");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -7,40 +7,79 @@ export default defineConfig({
|
||||
lib: {
|
||||
entry: resolve(__dirname, "src/index.ts"),
|
||||
name: "VitePluginTurborepoDeploy",
|
||||
fileName: (format) => `index.${format}.js`,
|
||||
formats: ["es", "cjs", "umd"],
|
||||
fileName: (format) => `index.${format === "es" ? "mjs" : "cjs"}`,
|
||||
formats: ["es", "cjs"],
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [
|
||||
// Vite
|
||||
"vite",
|
||||
"fs-extra",
|
||||
"simple-git",
|
||||
"chalk",
|
||||
"ora",
|
||||
"zod",
|
||||
// Node.js built-ins
|
||||
"path",
|
||||
"os",
|
||||
"crypto",
|
||||
"child_process",
|
||||
"util",
|
||||
"fs",
|
||||
"stream",
|
||||
"events",
|
||||
"zlib",
|
||||
"assert",
|
||||
"constants",
|
||||
"url",
|
||||
"buffer",
|
||||
"string_decoder",
|
||||
// Node.js prefixed modules
|
||||
"node:path",
|
||||
"node:os",
|
||||
"node:crypto",
|
||||
"node:child_process",
|
||||
"node:util",
|
||||
"node:fs",
|
||||
"node:fs/promises",
|
||||
"node:stream",
|
||||
"node:events",
|
||||
"node:zlib",
|
||||
"node:assert",
|
||||
"node:constants",
|
||||
"node:url",
|
||||
"node:buffer",
|
||||
"node:string_decoder",
|
||||
// Third-party dependencies
|
||||
"fs-extra",
|
||||
"simple-git",
|
||||
"chalk",
|
||||
"ora",
|
||||
"zod",
|
||||
"picomatch",
|
||||
"archiver",
|
||||
"yauzl",
|
||||
],
|
||||
output: {
|
||||
globals: {
|
||||
vite: "Vite",
|
||||
"fs-extra": "fsExtra",
|
||||
"simple-git": "simpleGit",
|
||||
chalk: "Chalk",
|
||||
ora: "Ora",
|
||||
zod: "Zod",
|
||||
path: "path",
|
||||
os: "os",
|
||||
crypto: "crypto",
|
||||
child_process: "childProcess",
|
||||
util: "util",
|
||||
fs: "fs",
|
||||
stream: "stream",
|
||||
events: "events",
|
||||
zlib: "zlib",
|
||||
assert: "assert",
|
||||
constants: "constants",
|
||||
url: "url",
|
||||
buffer: "buffer",
|
||||
string_decoder: "stringDecoder",
|
||||
"fs-extra": "fsExtra",
|
||||
"simple-git": "simpleGit",
|
||||
chalk: "Chalk",
|
||||
ora: "Ora",
|
||||
zod: "Zod",
|
||||
picomatch: "picomatch",
|
||||
archiver: "archiver",
|
||||
yauzl: "yauzl",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -50,9 +89,8 @@ export default defineConfig({
|
||||
plugins: [
|
||||
dts({
|
||||
insertTypesEntry: true,
|
||||
outputDir: "dist",
|
||||
outDir: "dist",
|
||||
staticImport: true,
|
||||
skipDiagnostics: false,
|
||||
}),
|
||||
],
|
||||
// 优化构建过程中的代码分析
|
||||
|
||||
6265
frontend/pnpm-lock.yaml
generated
6265
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user