@tnotesjs/core 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -1
- package/config/ConfigManager.ts +137 -0
- package/config/constants.ts +121 -0
- package/config/defaultConfig.ts +33 -4
- package/config/index.ts +31 -0
- package/config/templates.ts +49 -0
- package/core/GitManager.ts +513 -0
- package/core/NoteIndexCache.ts +200 -0
- package/core/NoteManager.ts +395 -0
- package/core/ProcessManager.ts +180 -0
- package/core/ReadmeGenerator.ts +215 -0
- package/core/TocGenerator.ts +212 -0
- package/core/index.ts +11 -0
- package/dist/{chunk-5Y5IYS6C.js → chunk-UJG3UU5X.js} +21 -5
- package/dist/{chunk-5QN44ES5.js → chunk-XCWFZLER.js} +48 -368
- package/dist/cli/index.js +26 -78
- package/dist/index.js +1 -1
- package/dist/vitepress/config/index.js +2 -2
- package/package.json +5 -2
- package/services/file-watcher/configChangeHandler.ts +64 -0
- package/services/file-watcher/eventScheduler.ts +179 -0
- package/services/file-watcher/folderChangeHandler.ts +325 -0
- package/services/file-watcher/fsWatcherAdapter.ts +128 -0
- package/services/file-watcher/globalUpdateCoordinator.ts +60 -0
- package/services/file-watcher/index.ts +7 -0
- package/services/file-watcher/internal.ts +79 -0
- package/services/file-watcher/readmeChangeHandler.ts +28 -0
- package/services/file-watcher/renameDetector.ts +120 -0
- package/services/file-watcher/service.ts +352 -0
- package/services/file-watcher/watchState.ts +194 -0
- package/services/git/index.ts +7 -0
- package/services/git/service.ts +114 -0
- package/services/index.ts +14 -0
- package/services/init-sub-repo/index.ts +2 -0
- package/services/init-sub-repo/initSubRepoLogic.test.ts +162 -0
- package/services/init-sub-repo/initSubRepoLogic.ts +303 -0
- package/services/init-sub-repo/service.ts +101 -0
- package/services/note/index.ts +7 -0
- package/services/note/service.ts +361 -0
- package/services/readme/index.ts +7 -0
- package/services/readme/service.ts +761 -0
- package/services/toc/index.ts +5 -0
- package/services/toc/moveTocInside.test.ts +73 -0
- package/services/toc/service.ts +759 -0
- package/services/vitepress/index.ts +7 -0
- package/services/vitepress/service.ts +339 -0
- package/types/config.ts +0 -3
- package/types/note.ts +0 -2
- package/utils/errorHandler.ts +174 -0
- package/utils/file.ts +17 -0
- package/utils/genHierarchicalSidebar.ts +69 -0
- package/utils/generateAnchor.ts +24 -0
- package/utils/getChangedIds.ts +35 -0
- package/utils/index.ts +71 -0
- package/utils/logger.ts +231 -0
- package/utils/markdown.ts +75 -0
- package/utils/migrateReadmeToToc.test.ts +111 -0
- package/utils/migrateReadmeToToc.ts +135 -0
- package/utils/parseArgs.ts +90 -0
- package/utils/parseReadmeCompletedNotes.test.ts +90 -0
- package/utils/parseReadmeCompletedNotes.ts +108 -0
- package/utils/portUtils.ts +113 -0
- package/utils/readmeHelpers.ts +190 -0
- package/utils/runCommand.ts +29 -0
- package/utils/tocHelpers.test.ts +278 -0
- package/utils/tocHelpers.ts +855 -0
- package/utils/tocNodeId.test.ts +60 -0
- package/utils/tocNodeId.ts +97 -0
- package/utils/validators.ts +102 -0
- package/vitepress/assets/icons/icon__cursor.svg +4 -0
- package/vitepress/assets/icons/index.ts +1 -0
- package/vitepress/components/Layout/AboutPanel.vue +0 -24
- package/vitepress/components/Layout/DocBeforeControls.vue +6 -14
- package/vitepress/components/Layout/DocFooter.vue +3 -3
- package/vitepress/components/Layout/Layout.vue +0 -30
- package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +20 -11
- package/vitepress/components/Layout/homeReadme.data.ts +0 -48
- package/vitepress/components/MarkMap/MarkMap.vue +6 -13
- package/vitepress/components/Settings/Settings.vue +76 -73
- package/vitepress/components/SidebarCard/FolderTreeItems.vue +16 -6
- package/vitepress/components/SidebarCard/SidebarCard.vue +21 -27
- package/vitepress/components/composables/useLocalIde.ts +83 -0
- package/vitepress/components/constants.ts +5 -6
- package/vitepress/components/utils/vscodePaths.test.ts +22 -0
- package/vitepress/components/utils/vscodePaths.ts +24 -2
- package/vitepress/theme/styles/doc-layout.scss +4 -4
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* services/vitepress/VitepressService.ts
|
|
3
|
+
*
|
|
4
|
+
* VitePress 服务 - 封装 VitePress 开发服务器相关的业务逻辑
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn } from 'child_process'
|
|
8
|
+
|
|
9
|
+
import { ConfigManager } from '../../config/ConfigManager'
|
|
10
|
+
import { ROOT_DIR_PATH } from '../../config/constants'
|
|
11
|
+
import { ProcessManager } from '../../core'
|
|
12
|
+
import { logger, isPortInUse, killPortProcess, waitForPort } from '../../utils'
|
|
13
|
+
|
|
14
|
+
/** VitePress 服务启动结果 */
|
|
15
|
+
export interface VitepressStartResult {
|
|
16
|
+
pid: number
|
|
17
|
+
version: string
|
|
18
|
+
/** 启动耗时(毫秒) */
|
|
19
|
+
elapsed: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class VitepressService {
|
|
23
|
+
/** VitePress 开发服务器默认端口 */
|
|
24
|
+
static readonly DEFAULT_DEV_PORT = 5173
|
|
25
|
+
|
|
26
|
+
/** VitePress 预览服务器默认端口 */
|
|
27
|
+
static readonly DEFAULT_PREVIEW_PORT = 4173
|
|
28
|
+
|
|
29
|
+
/** 开发服务器进程 ID 后缀 */
|
|
30
|
+
private static readonly PROCESS_ID_DEV_SUFFIX = 'vitepress-dev'
|
|
31
|
+
|
|
32
|
+
/** 预览服务器进程 ID 后缀 */
|
|
33
|
+
private static readonly PROCESS_ID_PREVIEW_SUFFIX = 'vitepress-preview'
|
|
34
|
+
|
|
35
|
+
/** 服务启动超时时间(毫秒) */
|
|
36
|
+
private static readonly SERVER_STARTUP_TIMEOUT = 60000
|
|
37
|
+
|
|
38
|
+
/** 端口释放等待超时时间(毫秒) */
|
|
39
|
+
private static readonly PORT_RELEASE_TIMEOUT = 3000
|
|
40
|
+
|
|
41
|
+
/** 进程清理等待时间(毫秒) */
|
|
42
|
+
private static readonly PROCESS_CLEANUP_DELAY = 3000
|
|
43
|
+
|
|
44
|
+
/** 显示启动服务状态行间隔(毫秒) */
|
|
45
|
+
private static readonly SERVER_STATUS_LINE_INTERVAL = 1000
|
|
46
|
+
|
|
47
|
+
/** 默认包管理器 */
|
|
48
|
+
private static readonly DEFAULT_PACKAGE_MANAGER = 'pnpm'
|
|
49
|
+
|
|
50
|
+
private processManager: ProcessManager
|
|
51
|
+
private configManager: ConfigManager
|
|
52
|
+
|
|
53
|
+
constructor() {
|
|
54
|
+
this.processManager = new ProcessManager()
|
|
55
|
+
this.configManager = ConfigManager.getInstance()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 启动 VitePress 开发服务器
|
|
60
|
+
* @returns 启动结果(服务就绪后返回),失败时返回 undefined
|
|
61
|
+
*/
|
|
62
|
+
async startServer(): Promise<VitepressStartResult | undefined> {
|
|
63
|
+
const port =
|
|
64
|
+
this.configManager.get('port') || VitepressService.DEFAULT_DEV_PORT
|
|
65
|
+
const repoName = this.configManager.get('repoName')
|
|
66
|
+
const processId = `${repoName}-${VitepressService.PROCESS_ID_DEV_SUFFIX}`
|
|
67
|
+
|
|
68
|
+
// 检查内存中的进程管理器(清理残留)
|
|
69
|
+
if (
|
|
70
|
+
this.processManager.has(processId) &&
|
|
71
|
+
this.processManager.isRunning(processId)
|
|
72
|
+
) {
|
|
73
|
+
this.processManager.kill(processId)
|
|
74
|
+
await new Promise((resolve) =>
|
|
75
|
+
setTimeout(resolve, VitepressService.PROCESS_CLEANUP_DELAY),
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 检查目标端口是否被占用,如果是则强制清理
|
|
80
|
+
if (isPortInUse(port)) {
|
|
81
|
+
logger.warn(`端口 ${port} 被占用,正在清理...`)
|
|
82
|
+
killPortProcess(port)
|
|
83
|
+
const available = await waitForPort(
|
|
84
|
+
port,
|
|
85
|
+
VitepressService.PORT_RELEASE_TIMEOUT,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if (available) {
|
|
89
|
+
logger.info(`端口 ${port} 已释放,继续启动服务`)
|
|
90
|
+
} else {
|
|
91
|
+
logger.warn(
|
|
92
|
+
`端口 ${port} 未确认释放,仍将尝试启动;如启动失败,请手动清理该端口`,
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 启动 VitePress 开发服务器
|
|
98
|
+
const pm =
|
|
99
|
+
this.configManager.get('packageManager') ||
|
|
100
|
+
VitepressService.DEFAULT_PACKAGE_MANAGER
|
|
101
|
+
const args = ['vitepress', 'dev', '--port', port.toString()]
|
|
102
|
+
|
|
103
|
+
const processInfo = this.processManager.spawn(processId, pm, args, {
|
|
104
|
+
cwd: ROOT_DIR_PATH,
|
|
105
|
+
stdio: ['inherit', 'pipe', 'pipe'], // stdin 继承,stdout/stderr 管道捕获
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// 等待服务就绪,显示启动状态
|
|
109
|
+
const serverInfo = await this.waitForServerReady(processInfo.process)
|
|
110
|
+
|
|
111
|
+
if (!processInfo.pid) return undefined
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
pid: processInfo.pid,
|
|
115
|
+
version: serverInfo.version,
|
|
116
|
+
elapsed: serverInfo.elapsed,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 等待服务就绪,显示启动状态
|
|
122
|
+
* @param childProcess - 子进程
|
|
123
|
+
*/
|
|
124
|
+
private waitForServerReady(
|
|
125
|
+
childProcess: import('child_process').ChildProcess,
|
|
126
|
+
): Promise<{ version: string; elapsed: number }> {
|
|
127
|
+
return new Promise((resolve) => {
|
|
128
|
+
const startTime = Date.now()
|
|
129
|
+
let serverReady = false
|
|
130
|
+
let version = ''
|
|
131
|
+
|
|
132
|
+
// 定时器:显示启动状态(真实的已用时间)
|
|
133
|
+
const statusTimer = setInterval(() => {
|
|
134
|
+
if (serverReady) {
|
|
135
|
+
clearInterval(statusTimer)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const elapsed = Date.now() - startTime
|
|
140
|
+
const seconds = (elapsed / 1000).toFixed(0)
|
|
141
|
+
// 使用 stderr 输出,避免与 VitePress 输出混在一起
|
|
142
|
+
process.stderr.clearLine?.(0)
|
|
143
|
+
process.stderr.cursorTo?.(0)
|
|
144
|
+
process.stderr.write(`⏳ 启动中: 已用 ${seconds}s...`)
|
|
145
|
+
}, VitepressService.SERVER_STATUS_LINE_INTERVAL)
|
|
146
|
+
|
|
147
|
+
// 处理输出
|
|
148
|
+
const handleOutput = (data: string) => {
|
|
149
|
+
const text = data.toString()
|
|
150
|
+
|
|
151
|
+
// 提取版本号
|
|
152
|
+
const versionMatch = text.match(/vitepress v([\d.]+)/)
|
|
153
|
+
if (versionMatch) {
|
|
154
|
+
version = versionMatch[1]
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 检测服务就绪
|
|
158
|
+
if (
|
|
159
|
+
!serverReady &&
|
|
160
|
+
(text.includes('Local:') ||
|
|
161
|
+
text.includes('http://localhost') ||
|
|
162
|
+
(text.includes('➜') && text.includes('Local')))
|
|
163
|
+
) {
|
|
164
|
+
serverReady = true
|
|
165
|
+
clearInterval(statusTimer)
|
|
166
|
+
|
|
167
|
+
// 清除状态行
|
|
168
|
+
process.stderr.clearLine?.(0)
|
|
169
|
+
process.stderr.cursorTo?.(0)
|
|
170
|
+
|
|
171
|
+
const elapsed = Date.now() - startTime
|
|
172
|
+
|
|
173
|
+
// 延迟 resolve,让后续输出完成
|
|
174
|
+
setTimeout(() => resolve({ version, elapsed }), 200)
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 服务就绪前只显示错误信息
|
|
179
|
+
if (!serverReady) {
|
|
180
|
+
if (
|
|
181
|
+
text.includes('error') ||
|
|
182
|
+
text.includes('Error') ||
|
|
183
|
+
(text.includes('Port') && text.includes('is in use'))
|
|
184
|
+
) {
|
|
185
|
+
process.stderr.clearLine?.(0)
|
|
186
|
+
process.stderr.cursorTo?.(0)
|
|
187
|
+
process.stdout.write(data)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 监听输出
|
|
193
|
+
if (childProcess.stdout) {
|
|
194
|
+
childProcess.stdout.setEncoding('utf8')
|
|
195
|
+
childProcess.stdout.on('data', handleOutput)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (childProcess.stderr) {
|
|
199
|
+
childProcess.stderr.setEncoding('utf8')
|
|
200
|
+
childProcess.stderr.on('data', handleOutput)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 超时处理
|
|
204
|
+
setTimeout(() => {
|
|
205
|
+
if (!serverReady) {
|
|
206
|
+
serverReady = true
|
|
207
|
+
clearInterval(statusTimer)
|
|
208
|
+
process.stderr.clearLine?.(0)
|
|
209
|
+
process.stderr.cursorTo?.(0)
|
|
210
|
+
logger.warn('启动超时,请检查 VitePress 输出')
|
|
211
|
+
resolve({ version, elapsed: VitepressService.SERVER_STARTUP_TIMEOUT })
|
|
212
|
+
}
|
|
213
|
+
}, VitepressService.SERVER_STARTUP_TIMEOUT)
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 构建生产版本
|
|
219
|
+
*/
|
|
220
|
+
build(): Promise<void> {
|
|
221
|
+
return new Promise((resolve, reject) => {
|
|
222
|
+
const pm =
|
|
223
|
+
this.configManager.get('packageManager') ||
|
|
224
|
+
VitepressService.DEFAULT_PACKAGE_MANAGER
|
|
225
|
+
const child = spawn(pm, ['vitepress', 'build'], {
|
|
226
|
+
cwd: ROOT_DIR_PATH,
|
|
227
|
+
shell: true,
|
|
228
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
229
|
+
env: {
|
|
230
|
+
...process.env,
|
|
231
|
+
NODE_OPTIONS: (process.env.NODE_OPTIONS || '').includes(
|
|
232
|
+
'--max-old-space-size',
|
|
233
|
+
)
|
|
234
|
+
? process.env.NODE_OPTIONS
|
|
235
|
+
: [process.env.NODE_OPTIONS, '--max-old-space-size=4096']
|
|
236
|
+
.filter(Boolean)
|
|
237
|
+
.join(' '),
|
|
238
|
+
},
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
// 过滤 VitePress 的 spinner 和状态输出,但保留我们的进度条
|
|
242
|
+
const filterOutput = (data: Buffer) => {
|
|
243
|
+
const str = data.toString()
|
|
244
|
+
|
|
245
|
+
// 允许我们的进度条和结果输出
|
|
246
|
+
if (
|
|
247
|
+
str.includes('🔨') ||
|
|
248
|
+
str.includes('✅') ||
|
|
249
|
+
str.includes('❌ 构建失败') ||
|
|
250
|
+
str.includes('📁') ||
|
|
251
|
+
str.includes('📊') ||
|
|
252
|
+
str.includes('📦') ||
|
|
253
|
+
str.includes('⏱️') ||
|
|
254
|
+
str.includes('⏳') ||
|
|
255
|
+
str.includes('Building [') ||
|
|
256
|
+
str.includes('error') ||
|
|
257
|
+
str.includes('Error')
|
|
258
|
+
) {
|
|
259
|
+
process.stdout.write(data)
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 过滤掉 VitePress 的 spinner 和状态输出
|
|
264
|
+
// 包括: ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ spinner 字符, ✓ 完成标记, vitepress 版本信息等
|
|
265
|
+
if (
|
|
266
|
+
/^[\s⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏✓\r\n]*$/.test(str) ||
|
|
267
|
+
str.includes('building client + server') ||
|
|
268
|
+
str.includes('rendering pages') ||
|
|
269
|
+
str.includes('generating sitemap') ||
|
|
270
|
+
str.includes('build complete in') ||
|
|
271
|
+
str.includes('vitepress v')
|
|
272
|
+
) {
|
|
273
|
+
return // 静默这些输出
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// 其他输出也静默(插件已经在内部拦截了)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
child.stdout?.on('data', filterOutput)
|
|
280
|
+
child.stderr?.on('data', filterOutput)
|
|
281
|
+
|
|
282
|
+
child.on('error', (err: Error) => {
|
|
283
|
+
reject(err)
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
child.on('close', (code: number) => {
|
|
287
|
+
if (code === 0) {
|
|
288
|
+
resolve()
|
|
289
|
+
} else {
|
|
290
|
+
reject(new Error(`Command failed with code ${code}`))
|
|
291
|
+
}
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* 预览构建后的站点
|
|
298
|
+
*/
|
|
299
|
+
async preview(): Promise<number | undefined> {
|
|
300
|
+
const repoName = this.configManager.get('repoName')
|
|
301
|
+
const processId = `${repoName}-${VitepressService.PROCESS_ID_PREVIEW_SUFFIX}`
|
|
302
|
+
const pm =
|
|
303
|
+
this.configManager.get('packageManager') ||
|
|
304
|
+
VitepressService.DEFAULT_PACKAGE_MANAGER
|
|
305
|
+
const args = ['vitepress', 'preview']
|
|
306
|
+
const previewPort = VitepressService.DEFAULT_PREVIEW_PORT
|
|
307
|
+
|
|
308
|
+
// 检查端口是否被占用
|
|
309
|
+
if (isPortInUse(previewPort)) {
|
|
310
|
+
logger.warn(`端口 ${previewPort} 已被占用,正在尝试清理...`)
|
|
311
|
+
const killed = killPortProcess(previewPort)
|
|
312
|
+
|
|
313
|
+
if (killed) {
|
|
314
|
+
// 等待端口释放
|
|
315
|
+
const available = await waitForPort(
|
|
316
|
+
previewPort,
|
|
317
|
+
VitepressService.PORT_RELEASE_TIMEOUT,
|
|
318
|
+
)
|
|
319
|
+
if (!available) {
|
|
320
|
+
logger.error(`端口 ${previewPort} 释放超时,请手动清理`)
|
|
321
|
+
return undefined
|
|
322
|
+
}
|
|
323
|
+
logger.info(`端口 ${previewPort} 已释放`)
|
|
324
|
+
} else {
|
|
325
|
+
logger.error(
|
|
326
|
+
`无法清理端口 ${previewPort},请手动执行: taskkill /F /PID <PID>`,
|
|
327
|
+
)
|
|
328
|
+
return undefined
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const processInfo = this.processManager.spawn(processId, pm, args, {
|
|
333
|
+
cwd: ROOT_DIR_PATH,
|
|
334
|
+
stdio: 'inherit',
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
return processInfo.pid
|
|
338
|
+
}
|
|
339
|
+
}
|
package/types/config.ts
CHANGED
package/types/note.ts
CHANGED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils/errorHandler.ts
|
|
3
|
+
*
|
|
4
|
+
* 统一的错误处理系统
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** TNotes 错误代码枚举 */
|
|
8
|
+
export enum ErrorCode {
|
|
9
|
+
// 文件系统错误
|
|
10
|
+
FILE_NOT_FOUND = 'FILE_NOT_FOUND',
|
|
11
|
+
FILE_READ_ERROR = 'FILE_READ_ERROR',
|
|
12
|
+
FILE_WRITE_ERROR = 'FILE_WRITE_ERROR',
|
|
13
|
+
|
|
14
|
+
// Git 相关错误
|
|
15
|
+
GIT_NOT_REPO = 'GIT_NOT_REPO',
|
|
16
|
+
GIT_COMMAND_FAILED = 'GIT_COMMAND_FAILED',
|
|
17
|
+
GIT_MERGE_CONFLICT = 'GIT_MERGE_CONFLICT',
|
|
18
|
+
|
|
19
|
+
// 笔记相关错误
|
|
20
|
+
NOTE_INDEX_INVALID = 'NOTE_INDEX_INVALID',
|
|
21
|
+
NOTE_CONFIG_INVALID = 'NOTE_CONFIG_INVALID',
|
|
22
|
+
NOTE_NOT_FOUND = 'NOTE_NOT_FOUND',
|
|
23
|
+
|
|
24
|
+
// 配置错误
|
|
25
|
+
CONFIG_INVALID = 'CONFIG_INVALID',
|
|
26
|
+
CONFIG_MISSING = 'CONFIG_MISSING',
|
|
27
|
+
|
|
28
|
+
// 命令执行错误
|
|
29
|
+
COMMAND_NOT_FOUND = 'COMMAND_NOT_FOUND',
|
|
30
|
+
COMMAND_FAILED = 'COMMAND_FAILED',
|
|
31
|
+
|
|
32
|
+
// 服务器错误
|
|
33
|
+
SERVER_START_FAILED = 'SERVER_START_FAILED',
|
|
34
|
+
SERVER_STOP_FAILED = 'SERVER_STOP_FAILED',
|
|
35
|
+
PORT_IN_USE = 'PORT_IN_USE',
|
|
36
|
+
|
|
37
|
+
// 未知错误
|
|
38
|
+
UNKNOWN = 'UNKNOWN',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* TNotes 自定义错误类
|
|
43
|
+
*/
|
|
44
|
+
class TNotesError extends Error {
|
|
45
|
+
constructor(
|
|
46
|
+
message: string,
|
|
47
|
+
public code: ErrorCode = ErrorCode.UNKNOWN,
|
|
48
|
+
public context?: Record<string, any>,
|
|
49
|
+
) {
|
|
50
|
+
super(message)
|
|
51
|
+
this.name = 'TNotesError'
|
|
52
|
+
|
|
53
|
+
// 保持正确的堆栈跟踪
|
|
54
|
+
if (Error.captureStackTrace) {
|
|
55
|
+
Error.captureStackTrace(this, TNotesError)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 统一的错误处理函数
|
|
62
|
+
*/
|
|
63
|
+
export function handleError(error: unknown, exitOnError = false): void {
|
|
64
|
+
if (error instanceof TNotesError) {
|
|
65
|
+
console.error(`❌ TNotesError`)
|
|
66
|
+
console.error(`错误码:${error.code}`)
|
|
67
|
+
console.error(`错误信息:${error.message}`)
|
|
68
|
+
|
|
69
|
+
if (error.context && Object.keys(error.context).length > 0) {
|
|
70
|
+
console.error('错误上下文信息:', error.context)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (error.stack && process.env.DEBUG) {
|
|
74
|
+
console.error('错误堆栈信息:', error.stack)
|
|
75
|
+
}
|
|
76
|
+
} else if (error instanceof Error) {
|
|
77
|
+
console.error(`❌ Error`)
|
|
78
|
+
console.error(`错误信息:${error.message}`)
|
|
79
|
+
|
|
80
|
+
if (error.stack && process.env.DEBUG) {
|
|
81
|
+
console.error('错误堆栈信息:', error.stack)
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
console.error('❌ 未知错误:', error)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (exitOnError) {
|
|
88
|
+
process.exit(1)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 创建特定类型的错误
|
|
94
|
+
*/
|
|
95
|
+
export const createError = {
|
|
96
|
+
fileNotFound: (path: string) =>
|
|
97
|
+
new TNotesError(`文件未找到:${path}`, ErrorCode.FILE_NOT_FOUND, {
|
|
98
|
+
path,
|
|
99
|
+
}),
|
|
100
|
+
|
|
101
|
+
fileReadError: (path: string, originalError?: Error) =>
|
|
102
|
+
new TNotesError(`读取文件失败:${path}`, ErrorCode.FILE_READ_ERROR, {
|
|
103
|
+
path,
|
|
104
|
+
originalError: originalError?.message,
|
|
105
|
+
}),
|
|
106
|
+
|
|
107
|
+
fileWriteError: (path: string, originalError?: Error) =>
|
|
108
|
+
new TNotesError(`写入文件失败:${path}`, ErrorCode.FILE_WRITE_ERROR, {
|
|
109
|
+
path,
|
|
110
|
+
originalError: originalError?.message,
|
|
111
|
+
}),
|
|
112
|
+
|
|
113
|
+
gitNotRepo: (dir: string) =>
|
|
114
|
+
new TNotesError(`不是一个 Git 仓库:${dir}`, ErrorCode.GIT_NOT_REPO, {
|
|
115
|
+
dir,
|
|
116
|
+
}),
|
|
117
|
+
|
|
118
|
+
gitCommandFailed: (command: string, dir: string, originalError?: Error) =>
|
|
119
|
+
new TNotesError(`Git 命令失败:${command}`, ErrorCode.GIT_COMMAND_FAILED, {
|
|
120
|
+
command,
|
|
121
|
+
dir,
|
|
122
|
+
originalError: originalError?.message,
|
|
123
|
+
}),
|
|
124
|
+
|
|
125
|
+
noteIndexInvalid: (noteIndex: string) =>
|
|
126
|
+
new TNotesError(
|
|
127
|
+
`无效的笔记索引:${noteIndex}`,
|
|
128
|
+
ErrorCode.NOTE_INDEX_INVALID,
|
|
129
|
+
{
|
|
130
|
+
noteIndex,
|
|
131
|
+
},
|
|
132
|
+
),
|
|
133
|
+
|
|
134
|
+
noteConfigInvalid: (notePath: string, reason?: string) =>
|
|
135
|
+
new TNotesError(
|
|
136
|
+
`无效的笔记配置:${notePath}`,
|
|
137
|
+
ErrorCode.NOTE_CONFIG_INVALID,
|
|
138
|
+
{ notePath, reason },
|
|
139
|
+
),
|
|
140
|
+
|
|
141
|
+
configInvalid: (field: string, reason: string) =>
|
|
142
|
+
new TNotesError(`无效的配置字段:${field}`, ErrorCode.CONFIG_INVALID, {
|
|
143
|
+
field,
|
|
144
|
+
reason,
|
|
145
|
+
}),
|
|
146
|
+
|
|
147
|
+
commandNotFound: (commandName: string) =>
|
|
148
|
+
new TNotesError(`未找到命令:${commandName}`, ErrorCode.COMMAND_NOT_FOUND, {
|
|
149
|
+
commandName,
|
|
150
|
+
}),
|
|
151
|
+
|
|
152
|
+
commandFailed: (
|
|
153
|
+
commandName: string,
|
|
154
|
+
exitCode?: number,
|
|
155
|
+
originalError?: Error,
|
|
156
|
+
) =>
|
|
157
|
+
new TNotesError(`命令执行失败:${commandName}`, ErrorCode.COMMAND_FAILED, {
|
|
158
|
+
commandName,
|
|
159
|
+
exitCode,
|
|
160
|
+
originalError: originalError?.message,
|
|
161
|
+
}),
|
|
162
|
+
|
|
163
|
+
serverStartFailed: (port: number, originalError?: Error) =>
|
|
164
|
+
new TNotesError(
|
|
165
|
+
`启动服务器失败:端口 ${port}`,
|
|
166
|
+
ErrorCode.SERVER_START_FAILED,
|
|
167
|
+
{ port, originalError: originalError?.message },
|
|
168
|
+
),
|
|
169
|
+
|
|
170
|
+
portInUse: (port: number) =>
|
|
171
|
+
new TNotesError(`端口 ${port} 已被占用`, ErrorCode.PORT_IN_USE, {
|
|
172
|
+
port,
|
|
173
|
+
}),
|
|
174
|
+
}
|
package/utils/file.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils/file.ts
|
|
3
|
+
*
|
|
4
|
+
* 文件操作工具函数
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'fs'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 确保目录存在
|
|
11
|
+
* @param dir - 目录路径
|
|
12
|
+
*/
|
|
13
|
+
export async function ensureDirectory(dir: string): Promise<void> {
|
|
14
|
+
if (!fs.existsSync(dir)) {
|
|
15
|
+
await fs.promises.mkdir(dir, { recursive: true })
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils/genHierarchicalSidebar.ts
|
|
3
|
+
*
|
|
4
|
+
* 生成层次化侧边栏
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** 侧边栏项类型 */
|
|
8
|
+
interface SidebarItem {
|
|
9
|
+
text: string
|
|
10
|
+
link?: string
|
|
11
|
+
collapsed?: boolean
|
|
12
|
+
items?: SidebarItem[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface StackItem {
|
|
16
|
+
level: number
|
|
17
|
+
node: SidebarItem
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 生成层次化侧边栏
|
|
22
|
+
* @param itemList - 笔记项列表
|
|
23
|
+
* @param titles - 标题列表
|
|
24
|
+
* @param titlesNotesCount - 每个标题下的笔记数量
|
|
25
|
+
* @param sidebarIsCollapsed - 侧边栏是否默认折叠
|
|
26
|
+
* @returns 层次化的侧边栏结构
|
|
27
|
+
*/
|
|
28
|
+
export const genHierarchicalSidebar = (
|
|
29
|
+
itemList: SidebarItem[],
|
|
30
|
+
titles: string[],
|
|
31
|
+
titlesNotesCount: number[],
|
|
32
|
+
sidebarIsCollapsed: boolean,
|
|
33
|
+
): SidebarItem[] => {
|
|
34
|
+
const stack: StackItem[] = []
|
|
35
|
+
const root: SidebarItem[] = []
|
|
36
|
+
|
|
37
|
+
titles.forEach((title, i) => {
|
|
38
|
+
const match = title.match(/^#+/)
|
|
39
|
+
if (!match) return
|
|
40
|
+
|
|
41
|
+
const level = match[0].length
|
|
42
|
+
const text = title.replace(/^#+\s*/, '')
|
|
43
|
+
const noteItems = itemList.splice(0, titlesNotesCount[i])
|
|
44
|
+
|
|
45
|
+
const node: SidebarItem = {
|
|
46
|
+
text,
|
|
47
|
+
collapsed: sidebarIsCollapsed,
|
|
48
|
+
items: noteItems.length > 0 ? noteItems : [],
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (i === 0 && level === 1) return
|
|
52
|
+
|
|
53
|
+
while (stack.length > 0 && stack[stack.length - 1].level >= level) {
|
|
54
|
+
stack.pop()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (stack.length === 0) {
|
|
58
|
+
root.push(node)
|
|
59
|
+
} else {
|
|
60
|
+
const parent = stack[stack.length - 1].node
|
|
61
|
+
if (!parent.items) parent.items = []
|
|
62
|
+
parent.items.push(node)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
stack.push({ level, node })
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
return root
|
|
69
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils/generateAnchor.ts
|
|
3
|
+
*
|
|
4
|
+
* 生成 GitHub 风格的锚点
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import GithubSlugger from 'github-slugger'
|
|
8
|
+
|
|
9
|
+
const slugger = new GithubSlugger()
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 生成 GitHub 风格的锚点
|
|
13
|
+
*
|
|
14
|
+
* !注意:锚点的生成规则要保持一致:
|
|
15
|
+
* - .vitepress/config/markdown.config.ts - markdown.anchor.slugify
|
|
16
|
+
* - .vitepress/tnotes/update.ts
|
|
17
|
+
*
|
|
18
|
+
* @param label - 标题文本
|
|
19
|
+
* @returns 生成的锚点字符串
|
|
20
|
+
*/
|
|
21
|
+
export const generateAnchor = (label: string): string => {
|
|
22
|
+
slugger.reset()
|
|
23
|
+
return slugger.slug(label)
|
|
24
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils/getChangedIds.ts
|
|
3
|
+
*
|
|
4
|
+
* 获取 Git 中变更的笔记 ID 集合
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execSync } from 'child_process'
|
|
8
|
+
import path from 'path'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 获取 Git 中变更的笔记 ID 集合
|
|
12
|
+
* @returns 变更的笔记 ID 集合
|
|
13
|
+
*/
|
|
14
|
+
export function getChangedIds(): Set<string> {
|
|
15
|
+
const changedFiles = execSync(
|
|
16
|
+
`git diff --name-only HEAD -- "notes/[0-9][0-9][0-9][0-9]*/README.md"`
|
|
17
|
+
// 根据当前仓库状态和最近一次提交之间的比较
|
|
18
|
+
)
|
|
19
|
+
.toString()
|
|
20
|
+
.split(/\r?\n/)
|
|
21
|
+
.filter(Boolean)
|
|
22
|
+
.map((fp) => fp.replace(/^"|"$/g, '')) // 去掉 Git 输出的双引号
|
|
23
|
+
.map((fp) => fp.split('/').join(path.sep)) // 转换为平台路径
|
|
24
|
+
|
|
25
|
+
const changedIds = changedFiles
|
|
26
|
+
.map((fp) => {
|
|
27
|
+
const parts = fp.split(path.sep)
|
|
28
|
+
const dirName = parts.find((p, i) => parts[i - 1] === 'notes')
|
|
29
|
+
return dirName?.slice(0, 4)
|
|
30
|
+
})
|
|
31
|
+
.filter((id): id is string => Boolean(id))
|
|
32
|
+
|
|
33
|
+
// 移除日志输出,由调用方决定是否输出
|
|
34
|
+
return new Set(changedIds)
|
|
35
|
+
}
|