Skip to content

Commit 29fb63d

Browse files
feat(analyze): implement project analysis in worker with progress reporting
1 parent 565d7c0 commit 29fb63d

File tree

6 files changed

+95
-19
lines changed

6 files changed

+95
-19
lines changed

electron/ipc/project.ts

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,61 @@
11
import { exec } from 'node:child_process'
22
import path from 'node:path'
3+
import { Worker } from 'node:worker_threads'
34

45
import { dialog, ipcMain } from 'electron'
56
import fs from 'fs-extra'
67
import trash from 'trash'
78

89
import { projectsFilePath } from '../utils/dataPath'
910
import type { LinguistResult } from '../utils/linguist'
10-
import { analyzeFolder } from '../utils/linguist'
1111

1212
// 传入项目根目录,获取项目各编程语言的占比
13-
ipcMain.handle('project:analyze', async (_, folderPath): Promise<LinguistResult | { error: string }> => {
14-
try {
15-
const stat = await fs.stat(folderPath)
13+
ipcMain.handle('project:analyze', async (event, folderPath): Promise<LinguistResult | { error: string }> => {
14+
return await new Promise((resolve) => {
15+
try {
16+
const workerUrl = new URL('../workers/linguistAnalyze.worker.js', import.meta.url)
17+
const worker = new Worker(workerUrl, { type: 'module', workerData: { folderPath } } as any)
18+
19+
const cleanup = () => worker.removeAllListeners()
20+
const send = (stage: 'start' | 'checking' | 'analyzing' | 'done') => {
21+
try {
22+
event.sender.send('project:analyze:progress', {
23+
folderPath,
24+
stage,
25+
})
26+
}
27+
catch {}
28+
}
1629

17-
// 检查是否是目录
18-
if (!stat.isDirectory()) {
19-
return { error: 'Provided path is not a directory' }
30+
worker.on('message', (msg: any) => {
31+
if (msg?.type === 'progress') {
32+
send(msg.stage)
33+
}
34+
else if (msg?.type === 'result') {
35+
cleanup()
36+
resolve(msg.result as LinguistResult)
37+
}
38+
else if (msg?.type === 'error') {
39+
cleanup()
40+
resolve({ error: msg.error || 'analyze error' })
41+
}
42+
})
43+
44+
worker.once('error', (err) => {
45+
cleanup()
46+
resolve({ error: String(err) })
47+
})
48+
worker.once('exit', (code) => {
49+
if (code !== 0) {
50+
cleanup()
51+
resolve({ error: `analyze worker exited with code ${code}` })
52+
}
53+
})
2054
}
21-
22-
// 如果路径是有效的目录,继续分析
23-
return analyzeFolder(folderPath)
24-
}
25-
catch (error) {
26-
return { error: `Error checking folder path:${error}` }
27-
}
55+
catch (e) {
56+
resolve({ error: `Error starting analyze worker: ${String(e)}` })
57+
}
58+
})
2859
})
2960

3061
// 使用IDE打开项目

electron/ipc/scanner.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ ipcMain.handle('scanner:scan', async (_event, payload: ScanPayload): Promise<Sca
2121
return new Promise((resolve, reject) => {
2222
try {
2323
const workerUrl = new URL('../workers/projectScanner.worker.js', import.meta.url)
24-
const worker = new Worker(workerUrl, { type: 'module', workerData: payload })
24+
const worker = new Worker(workerUrl, { type: 'module', workerData: payload } as any)
2525

2626
const items: ScanResultItem[] = []
2727
const cleanup = () => worker.removeAllListeners()
@@ -62,7 +62,7 @@ let nextSessionId = 1
6262
ipcMain.handle('scanner:start', (event, payload: ScanPayload): { sessionId: number } => {
6363
const sessionId = nextSessionId++
6464
const workerUrl = new URL('../workers/projectScanner.worker.js', import.meta.url)
65-
const worker = new Worker(workerUrl, { type: 'module', workerData: payload })
65+
const worker = new Worker(workerUrl, { type: 'module', workerData: payload } as any)
6666

6767
sessions.set(sessionId, worker)
6868

electron/preload.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ contextBridge.exposeInMainWorld('api', {
1414

1515
// project
1616
analyzeProject: (folderPath: string) => ipcRenderer.invoke('project:analyze', folderPath),
17+
onProjectAnalyzeProgress: (cb: (data: { folderPath: string, stage: 'start' | 'checking' | 'analyzing' | 'done' }) => void) => {
18+
const handler = (_: any, data: any) => cb(data)
19+
ipcRenderer.on('project:analyze:progress', handler)
20+
return () => ipcRenderer.removeListener('project:analyze:progress', handler)
21+
},
1722
openProject: (idePath: string, projectPath: string) => ipcRenderer.invoke('project:open', idePath, projectPath),
1823
deleteProject: (projectPath: string) => ipcRenderer.invoke('project:delete', projectPath),
1924
importProject: () => ipcRenderer.invoke('project:import'),
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import * as fs from 'node:fs/promises'
2+
import { parentPort, workerData } from 'node:worker_threads'
3+
4+
import type { LinguistResult } from '../utils/linguist'
5+
import { analyzeFolder } from '../utils/linguist'
6+
7+
interface Payload { folderPath: string }
8+
9+
type WorkerMsg
10+
= | { type: 'progress', stage: 'start' | 'checking' | 'analyzing' | 'done' }
11+
| { type: 'result', result: LinguistResult }
12+
| { type: 'error', error: string }
13+
14+
async function run() {
15+
const { folderPath } = workerData as Payload
16+
17+
try {
18+
parentPort?.postMessage({ type: 'progress', stage: 'start' } satisfies WorkerMsg)
19+
20+
// 轻量校验,避免主线程重复 IO
21+
parentPort?.postMessage({ type: 'progress', stage: 'checking' } satisfies WorkerMsg)
22+
const stat = await fs.stat(folderPath)
23+
if (!stat.isDirectory())
24+
throw new Error('Provided path is not a directory')
25+
26+
parentPort?.postMessage({ type: 'progress', stage: 'analyzing' } satisfies WorkerMsg)
27+
const result = await analyzeFolder(folderPath)
28+
29+
parentPort?.postMessage({ type: 'result', result } satisfies WorkerMsg)
30+
parentPort?.postMessage({ type: 'progress', stage: 'done' } satisfies WorkerMsg)
31+
}
32+
catch (e: any) {
33+
parentPort?.postMessage({ type: 'error', error: e?.message || String(e) } satisfies WorkerMsg)
34+
}
35+
}
36+
37+
run()

src/global.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ declare global {
1717

1818
// project
1919
analyzeProject: (folderPath: string) => Promise<LinguistResult | { error: string }>
20+
onProjectAnalyzeProgress: (cb: (data: { folderPath: string, stage: 'start' | 'checking' | 'analyzing' | 'done' }) => void) => () => void
2021
openProject: (idePath: string, projectPath: string) => Promise<string>
2122
deleteProject: (projectPath: string) => Promise<boolean>
2223
importProject: () => Promise<boolean>

src/services/languageAnalyzer.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { JeDropdownOptionGroupProps, JeDropdownOptionProps } from 'jetv-ui'
22

33
import type { languagesGroupItem } from '~/constants/localProject'
44
import { t } from '~/utils/i18n'
5-
import type { LinguistLanguageResult, LinguistResult } from '~/views/ProjectConfig/types'
5+
import type { LinguistLanguageResult } from '~/views/ProjectConfig/types'
66

77
/**
88
* 用于分析项目语言的工具类
@@ -64,8 +64,10 @@ export class LanguageAnalyzer {
6464
* @returns {Promise<Record<string, LinguistLanguageResult>>} - 语言分析结果
6565
*/
6666
private async getLanguagesResult(): Promise<Record<string, LinguistLanguageResult>> {
67-
const { languages } = await window.api.analyzeProject(this.folderPath) as { languages: LinguistResult['languages'] }
68-
return languages.results
67+
const res = await window.api.analyzeProject(this.folderPath) as any
68+
if (!res || 'error' in res || !res.languages)
69+
return {}
70+
return res.languages.results as Record<string, LinguistLanguageResult>
6971
}
7072

7173
/**

0 commit comments

Comments
 (0)