@tnotesjs/core 0.0.5 → 0.0.7

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.
Files changed (113) hide show
  1. package/README.md +84 -84
  2. package/dist/vitepress/config/index.js +4 -1
  3. package/package.json +1 -1
  4. package/types/config.ts +61 -61
  5. package/types/index.ts +11 -11
  6. package/types/note.ts +33 -33
  7. package/vitepress/assets/icons/icon__check.svg +3 -3
  8. package/vitepress/assets/icons/icon__clipboard.svg +7 -7
  9. package/vitepress/assets/icons/icon__close.svg +1 -1
  10. package/vitepress/assets/icons/icon__collapse.svg +1 -1
  11. package/vitepress/assets/icons/icon__confirm.svg +1 -1
  12. package/vitepress/assets/icons/icon__copy.svg +4 -4
  13. package/vitepress/assets/icons/icon__focus.svg +1 -1
  14. package/vitepress/assets/icons/icon__fold.svg +3 -3
  15. package/vitepress/assets/icons/icon__folder.svg +1 -1
  16. package/vitepress/assets/icons/icon__fullscreen.svg +1 -1
  17. package/vitepress/assets/icons/icon__fullscreen_exit.svg +1 -1
  18. package/vitepress/assets/icons/icon__github.svg +3 -3
  19. package/vitepress/assets/icons/icon__mindmap.svg +1 -1
  20. package/vitepress/assets/icons/icon__next.svg +1 -1
  21. package/vitepress/assets/icons/icon__number_gray.svg +1 -1
  22. package/vitepress/assets/icons/icon__number_purple.svg +1 -1
  23. package/vitepress/assets/icons/icon__prev.svg +1 -1
  24. package/vitepress/assets/icons/icon__restore.svg +1 -1
  25. package/vitepress/assets/icons/icon__rotate.svg +4 -4
  26. package/vitepress/assets/icons/icon__search.svg +1 -1
  27. package/vitepress/assets/icons/icon__sidebar_collapsed.svg +1 -1
  28. package/vitepress/assets/icons/icon__sidebar_opened.svg +1 -1
  29. package/vitepress/assets/icons/icon__totop.svg +5 -5
  30. package/vitepress/assets/icons/icon__vscode.svg +5 -5
  31. package/vitepress/assets/icons/icon__zoom_fit.svg +1 -1
  32. package/vitepress/assets/icons/icon__zoom_in.svg +1 -1
  33. package/vitepress/assets/icons/icon__zoom_out.svg +1 -1
  34. package/vitepress/assets/icons/icon__zoom_reset.svg +1 -1
  35. package/vitepress/assets/icons/index.ts +38 -38
  36. package/vitepress/components/BilibiliOutsidePlayer/BilibiliOutsidePlayer.vue +20 -20
  37. package/vitepress/components/CodeBlockFullscreen/CodeBlockFullscreen.vue +373 -373
  38. package/vitepress/components/CodeBlockFullscreen/index.ts +115 -115
  39. package/vitepress/components/CodeBlockFullscreen/styles.css +64 -64
  40. package/vitepress/components/Discussions/Discussions.module.scss +32 -32
  41. package/vitepress/components/Discussions/Discussions.vue +211 -211
  42. package/vitepress/components/EnWordList/EnWordList.module.scss +124 -124
  43. package/vitepress/components/EnWordList/EnWordList.vue +543 -543
  44. package/vitepress/components/EnWordList/RightClickMenu.module.scss +22 -22
  45. package/vitepress/components/EnWordList/RightClickMenu.vue +66 -66
  46. package/vitepress/components/Footprints/Footprints.module.scss +93 -93
  47. package/vitepress/components/Footprints/Footprints.vue +377 -377
  48. package/vitepress/components/Layout/AboutModal.module.scss +233 -233
  49. package/vitepress/components/Layout/AboutModal.vue +105 -105
  50. package/vitepress/components/Layout/AboutPanel.vue +266 -266
  51. package/vitepress/components/Layout/ContentCollapse.vue +603 -603
  52. package/vitepress/components/Layout/CustomSidebar.vue +605 -605
  53. package/vitepress/components/Layout/DocBeforeControls.vue +149 -139
  54. package/vitepress/components/Layout/DocFooter.vue +233 -233
  55. package/vitepress/components/Layout/ImagePreview.module.scss +201 -201
  56. package/vitepress/components/Layout/ImagePreview.vue +281 -281
  57. package/vitepress/components/Layout/Layout.module.scss +661 -661
  58. package/vitepress/components/Layout/Layout.vue +542 -542
  59. package/vitepress/components/Layout/NoteStatus.vue +140 -140
  60. package/vitepress/components/Layout/SidebarItems.vue +263 -263
  61. package/vitepress/components/Layout/SidebarNavBefore.vue +92 -92
  62. package/vitepress/components/Layout/Swiper.vue +167 -167
  63. package/vitepress/components/Layout/ToggleFullContent.module.scss +11 -11
  64. package/vitepress/components/Layout/ToggleFullContent.vue +34 -34
  65. package/vitepress/components/Layout/ToggleSidebar.module.scss +11 -11
  66. package/vitepress/components/Layout/ToggleSidebar.vue +35 -35
  67. package/vitepress/components/Layout/composables/useCollapseControl.ts +88 -88
  68. package/vitepress/components/Layout/composables/useNoteConfig.ts +121 -121
  69. package/vitepress/components/Layout/composables/useNoteSave.ts +173 -173
  70. package/vitepress/components/Layout/composables/useNoteValidation.ts +85 -85
  71. package/vitepress/components/Layout/composables/useRedirect.ts +110 -110
  72. package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +85 -85
  73. package/vitepress/components/Layout/homeReadme.data.ts +124 -124
  74. package/vitepress/components/LoadingPage/LoadingPage.vue +192 -192
  75. package/vitepress/components/MarkMap/MarkMap.module.scss +159 -159
  76. package/vitepress/components/MarkMap/MarkMap.vue +404 -404
  77. package/vitepress/components/Mermaid/Mermaid.module.scss +275 -275
  78. package/vitepress/components/Mermaid/Mermaid.vue +364 -364
  79. package/vitepress/components/NotesTable/NotesTable.module.scss +77 -77
  80. package/vitepress/components/NotesTable/NotesTable.vue +98 -98
  81. package/vitepress/components/NotesTable/README.md +67 -67
  82. package/vitepress/components/Settings/Settings.module.scss +433 -433
  83. package/vitepress/components/Settings/Settings.vue +306 -306
  84. package/vitepress/components/SidebarCard/MindMapView.vue +483 -483
  85. package/vitepress/components/SidebarCard/NotesTrendChart.vue +108 -108
  86. package/vitepress/components/SidebarCard/SidebarCard.vue +948 -948
  87. package/vitepress/components/Tooltip/Tooltip.vue +70 -70
  88. package/vitepress/components/constants.ts +91 -91
  89. package/vitepress/components/notesConfig.data.ts +73 -73
  90. package/vitepress/components/sidebar.data.ts +59 -59
  91. package/vitepress/components/tnotes-config.data.ts +21 -21
  92. package/vitepress/components/utils.ts +26 -26
  93. package/vitepress/config/index.ts +160 -141
  94. package/vitepress/configs/constants.ts +26 -26
  95. package/vitepress/configs/head.config.ts +25 -25
  96. package/vitepress/configs/index.ts +9 -9
  97. package/vitepress/configs/markdown-it.d.ts +23 -23
  98. package/vitepress/configs/markdown.config.ts +366 -366
  99. package/vitepress/configs/theme.config.ts +108 -108
  100. package/vitepress/plugins/buildProgressPlugin.ts +470 -470
  101. package/vitepress/plugins/getNoteByConfigIdPlugin.ts +107 -107
  102. package/vitepress/plugins/renameNotePlugin.ts +60 -60
  103. package/vitepress/plugins/updateConfigPlugin.ts +63 -63
  104. package/vitepress/theme/index.ts +95 -95
  105. package/vitepress/theme/styles/base.scss +50 -50
  106. package/vitepress/theme/styles/components/404.scss +31 -31
  107. package/vitepress/theme/styles/components/collapse.scss +175 -175
  108. package/vitepress/theme/styles/components/markmap.scss +101 -101
  109. package/vitepress/theme/styles/components/swiper.scss +255 -255
  110. package/vitepress/theme/styles/index.scss +25 -25
  111. package/vitepress/theme/styles/layout.scss +62 -62
  112. package/vitepress/theme/styles/utilities.scss +39 -39
  113. package/vitepress/theme/styles/vitepress-override.scss +25 -25
@@ -1,470 +1,470 @@
1
- /**
2
- * .vitepress/tnotes/vitepress/plugins/buildProgressPlugin.ts
3
- *
4
- * 构建进度插件 - 仅在 build 模式下显示真实进度
5
- *
6
- * 基于 vite-plugin-progress 源码简化实现
7
- * https://github.com/jeddygong/vite-plugin-progress
8
- */
9
-
10
- import type { Plugin } from 'vite'
11
- import {
12
- existsSync,
13
- readFileSync,
14
- writeFileSync,
15
- mkdirSync,
16
- readdirSync,
17
- } from 'fs'
18
- import { join } from 'path'
19
-
20
- /** 缓存目录和文件路径 */
21
- const CACHE_DIR = join(process.cwd(), 'node_modules', '.tnotes-progress')
22
- const CACHE_FILE = join(CACHE_DIR, 'build-cache.json')
23
-
24
- /** 缓存数据结构 */
25
- interface CacheData {
26
- transformCount: number
27
- chunkCount: number
28
- /** transform 阶段耗时占总构建时间的比例(0-1),用于下次按时间权重分配进度 */
29
- transformEndRatio?: number
30
- }
31
-
32
- /** 插件配置选项 */
33
- interface BuildProgressOptions {
34
- width?: number
35
- complete?: string
36
- incomplete?: string
37
- }
38
-
39
- /**
40
- * 读取缓存数据
41
- */
42
- function getCacheData(): CacheData {
43
- try {
44
- if (existsSync(CACHE_FILE)) {
45
- return JSON.parse(readFileSync(CACHE_FILE, 'utf-8'))
46
- }
47
- } catch {
48
- // 忽略错误
49
- }
50
- return { transformCount: 0, chunkCount: 0 }
51
- }
52
-
53
- /**
54
- * 写入缓存数据
55
- */
56
- function setCacheData(data: CacheData): void {
57
- try {
58
- if (!existsSync(CACHE_DIR)) {
59
- mkdirSync(CACHE_DIR, { recursive: true })
60
- }
61
- writeFileSync(CACHE_FILE, JSON.stringify(data), 'utf-8')
62
- } catch {
63
- // 忽略错误
64
- }
65
- }
66
-
67
- /**
68
- * 扫描源目录文件数量
69
- */
70
- function countSourceFiles(srcDir: string): number {
71
- let count = 0
72
- const extensions = /\.(vue|ts|js|jsx|tsx|css|scss|sass|styl|less|md)$/i
73
-
74
- const scan = (dir: string) => {
75
- try {
76
- const entries = readdirSync(dir, { withFileTypes: true })
77
- for (const entry of entries) {
78
- const fullPath = join(dir, entry.name)
79
- if (
80
- entry.isDirectory() &&
81
- !entry.name.startsWith('.') &&
82
- entry.name !== 'node_modules'
83
- ) {
84
- scan(fullPath)
85
- } else if (entry.isFile() && extensions.test(entry.name)) {
86
- count++
87
- }
88
- }
89
- } catch {
90
- // 忽略错误
91
- }
92
- }
93
-
94
- scan(srcDir)
95
- return count
96
- }
97
-
98
- // ============ 全局状态 ============
99
- let globalStartTime = 0
100
- let globalTransformCount = 0
101
- let globalChunkCount = 0
102
- let globalHasError = false
103
- let globalIsBuilding = false
104
- let globalOutDir = ''
105
- let globalLastPercent = 0
106
- let globalFileCount = 0
107
- let globalLastLoggedPercent = -1 // 非 TTY 环境下上次输出的百分比区间,初始为 -1 以便第一次输出
108
- let globalLastOutputTime = 0 // 非 TTY 环境下上次输出时间戳,用于时间节流
109
- let globalTransformEndTime = 0 // transform 阶段结束时间戳
110
- let globalLastHookTime = 0 // 最后一次 Vite hook 触发时间
111
- let globalStallTimer: ReturnType<typeof setInterval> | null = null
112
-
113
- /** 检测是否支持单行刷新(交互式终端) */
114
- const isTTY = !!(process.stdout.isTTY && process.stderr.isTTY)
115
-
116
- /** 检测是否在 CI 环境中运行 */
117
- const isCI = !!(
118
- process.env.CI ||
119
- process.env.GITHUB_ACTIONS ||
120
- process.env.GITLAB_CI ||
121
- process.env.CIRCLECI ||
122
- process.env.TRAVIS ||
123
- process.env.JENKINS_URL
124
- )
125
-
126
- /** 是否使用单行刷新模式(TTY 且非 CI) */
127
- const useSingleLineMode = isTTY && !isCI
128
-
129
- // 保存原始输出函数
130
- let originalStdoutWrite: typeof process.stdout.write | null = null
131
- let originalStderrWrite: typeof process.stderr.write | null = null
132
-
133
- /**
134
- * 拦截输出
135
- */
136
- function interceptOutput() {
137
- if (originalStdoutWrite) return
138
-
139
- originalStdoutWrite = process.stdout.write.bind(process.stdout)
140
- originalStderrWrite = process.stderr.write.bind(process.stderr)
141
-
142
- const filter = (chunk: string | Uint8Array): boolean => {
143
- const str = chunk.toString()
144
- // 只允许我们的输出
145
- return (
146
- str.includes('🔨') ||
147
- str.includes('✅ 构建成功') ||
148
- str.includes('❌ 构建失败') ||
149
- str.includes('📁') ||
150
- str.includes('📊') ||
151
- str.includes('📦') ||
152
- str.includes('⏱️') ||
153
- str.includes('\x1b[2K') // 允许清屏指令
154
- )
155
- }
156
-
157
- process.stdout.write = ((
158
- chunk: string | Uint8Array,
159
- encodingOrCallback?: BufferEncoding | ((err?: Error | null) => void),
160
- callback?: (err?: Error | null) => void,
161
- ): boolean => {
162
- if (filter(chunk)) {
163
- return originalStdoutWrite!(chunk, encodingOrCallback as any, callback)
164
- }
165
- if (typeof encodingOrCallback === 'function') encodingOrCallback()
166
- else if (callback) callback()
167
- return true
168
- }) as typeof process.stdout.write
169
-
170
- process.stderr.write = ((
171
- chunk: string | Uint8Array,
172
- encodingOrCallback?: BufferEncoding | ((err?: Error | null) => void),
173
- callback?: (err?: Error | null) => void,
174
- ): boolean => {
175
- const str = chunk.toString()
176
- // 允许进度条和真正的错误
177
- if (filter(chunk) || str.toLowerCase().includes('error')) {
178
- return originalStderrWrite!(chunk, encodingOrCallback as any, callback)
179
- }
180
- if (typeof encodingOrCallback === 'function') encodingOrCallback()
181
- else if (callback) callback()
182
- return true
183
- }) as typeof process.stderr.write
184
- }
185
-
186
- /**
187
- * 恢复输出
188
- */
189
- function restoreOutput() {
190
- if (originalStdoutWrite) {
191
- process.stdout.write = originalStdoutWrite
192
- originalStdoutWrite = null
193
- }
194
- if (originalStderrWrite) {
195
- process.stderr.write = originalStderrWrite
196
- originalStderrWrite = null
197
- }
198
- }
199
-
200
- /**
201
- * 渲染进度条
202
- */
203
- function renderProgress(
204
- percent: number,
205
- transforms: string,
206
- chunks: string,
207
- width: number,
208
- complete: string,
209
- incomplete: string,
210
- isFinal: boolean = false,
211
- ) {
212
- if (!originalStderrWrite) return
213
-
214
- // 非单行模式下,只在 10% 间隔输出进度,并附加时间节流避免多行显示相同耗时
215
- if (!useSingleLineMode && !isFinal) {
216
- const currentPercent = Math.floor(percent * 100)
217
- const currentBucket = Math.floor(currentPercent / 10) * 10
218
- const now = Date.now()
219
- if (
220
- currentBucket <= globalLastLoggedPercent ||
221
- now - globalLastOutputTime < 500
222
- ) {
223
- return
224
- }
225
- globalLastLoggedPercent = currentBucket
226
- globalLastOutputTime = now
227
- }
228
-
229
- const filled = Math.floor(percent * width)
230
- const empty = width - filled
231
- const bar = complete.repeat(filled) + incomplete.repeat(empty)
232
- const percentStr = (percent * 100).toFixed(0).padStart(3, ' ')
233
- const elapsed = ((Date.now() - globalStartTime) / 1000).toFixed(1)
234
-
235
- // 格式: Building [...] 100% | Transforms: x/y | Chunks: x/y | Time: xs
236
- // 单行模式使用回车覆盖,否则直接换行
237
- const prefix = useSingleLineMode ? '\r\x1b[2K' : ''
238
- const ending = isFinal || !useSingleLineMode ? '\n' : ''
239
- const line = `${prefix}Building [${bar}] ${percentStr}% | Transforms: ${transforms} | Chunks: ${chunks} | Time: ${elapsed}s${ending}`
240
- originalStderrWrite(line)
241
- }
242
-
243
- /**
244
- * 创建构建进度插件
245
- */
246
- export function buildProgressPlugin(
247
- options: BuildProgressOptions = {},
248
- ): Plugin {
249
- const { width = 40, complete = '█', incomplete = '░' } = options
250
-
251
- const cache = getCacheData()
252
- const hasCache = cache.transformCount > 0
253
-
254
- return {
255
- name: 'tnotes-build-progress',
256
- enforce: 'pre',
257
- apply: 'build',
258
-
259
- config(config, { command }) {
260
- if (command === 'build') {
261
- config.logLevel = 'silent'
262
-
263
- if (!globalIsBuilding) {
264
- globalIsBuilding = true
265
- globalStartTime = Date.now()
266
- globalTransformCount = 0
267
- globalChunkCount = 0
268
- globalHasError = false
269
- globalLastPercent = 0
270
- globalLastLoggedPercent = -1
271
- globalLastOutputTime = 0
272
- globalTransformEndTime = 0
273
- globalLastHookTime = Date.now()
274
- globalOutDir = config.build?.outDir || 'dist'
275
-
276
- if (!hasCache) {
277
- globalFileCount = countSourceFiles(process.cwd())
278
- }
279
-
280
- interceptOutput()
281
-
282
- // 停滞填充计时器:当 Vite hook 超过 2 秒没触发时(如 VitePress 渲染页面阶段),
283
- // 缓慢推进进度条,避免长时间卡住不动
284
- globalStallTimer = setInterval(() => {
285
- if (
286
- !globalIsBuilding ||
287
- globalLastPercent <= 0 ||
288
- globalLastPercent >= 0.98
289
- )
290
- return
291
- const now = Date.now()
292
- if (now - globalLastHookTime < 2000) return
293
-
294
- // 每次推进剩余距离的 2%,形成自然减速曲线
295
- const remaining = 0.98 - globalLastPercent
296
- globalLastPercent += remaining * 0.02
297
-
298
- const transformsStr = hasCache
299
- ? `${globalTransformCount}/${cache.transformCount * 2}`
300
- : `${globalTransformCount}`
301
- const chunksStr = hasCache
302
- ? `${globalChunkCount}/${cache.chunkCount * 2}`
303
- : `${globalChunkCount}`
304
-
305
- renderProgress(
306
- globalLastPercent,
307
- transformsStr,
308
- chunksStr,
309
- width,
310
- complete,
311
- incomplete,
312
- )
313
- }, 1000)
314
- }
315
- }
316
- },
317
-
318
- transform(_code, _id) {
319
- globalTransformCount++
320
- globalLastHookTime = Date.now()
321
-
322
- // 根据缓存的时间比例确定 transform 阶段在进度条中的权重
323
- // 例如 transform 占总时间 37% → 进度条 0-36%,chunk 占 63% → 进度条 36-98%
324
- const transformWeight =
325
- hasCache && cache.transformEndRatio != null
326
- ? Math.max(0.1, Math.min(0.85, cache.transformEndRatio * 0.98))
327
- : 0.85
328
-
329
- if (hasCache) {
330
- const totalTransforms = cache.transformCount * 2
331
- globalLastPercent = Math.min(
332
- transformWeight,
333
- (globalTransformCount / totalTransforms) * transformWeight,
334
- )
335
- } else {
336
- // 无缓存模式:渐近曲线平滑逼近上限
337
- const k = 500
338
- globalLastPercent = Math.min(
339
- transformWeight,
340
- (transformWeight * globalTransformCount) / (globalTransformCount + k),
341
- )
342
- }
343
-
344
- const transformsStr = hasCache
345
- ? `${globalTransformCount}/${cache.transformCount * 2}`
346
- : `${globalTransformCount}`
347
- const chunksStr = hasCache
348
- ? `${globalChunkCount}/${cache.chunkCount * 2}`
349
- : `${globalChunkCount}`
350
-
351
- renderProgress(
352
- globalLastPercent,
353
- transformsStr,
354
- chunksStr,
355
- width,
356
- complete,
357
- incomplete,
358
- )
359
-
360
- return null
361
- },
362
-
363
- renderChunk() {
364
- globalChunkCount++
365
- globalLastHookTime = Date.now()
366
-
367
- // 记录 transform 阶段结束时间(第一次进入 renderChunk)
368
- if (!globalTransformEndTime) {
369
- globalTransformEndTime = Date.now()
370
- }
371
-
372
- const transformWeight =
373
- hasCache && cache.transformEndRatio != null
374
- ? Math.max(0.1, Math.min(0.85, cache.transformEndRatio * 0.98))
375
- : 0.85
376
- const chunkWeight = 0.98 - transformWeight
377
-
378
- if (hasCache) {
379
- const totalChunks = cache.chunkCount * 2
380
- globalLastPercent = Math.min(
381
- 0.98,
382
- transformWeight + (globalChunkCount / totalChunks) * chunkWeight,
383
- )
384
- } else {
385
- globalLastPercent = Math.max(globalLastPercent, transformWeight)
386
- globalLastPercent = Math.min(0.98, globalLastPercent + 0.00005)
387
- }
388
-
389
- const transformsStr = hasCache
390
- ? `${globalTransformCount}/${cache.transformCount * 2}`
391
- : `${globalTransformCount}`
392
- const chunksStr = hasCache
393
- ? `${globalChunkCount}/${cache.chunkCount * 2}`
394
- : `${globalChunkCount}`
395
-
396
- renderProgress(
397
- globalLastPercent,
398
- transformsStr,
399
- chunksStr,
400
- width,
401
- complete,
402
- incomplete,
403
- )
404
-
405
- return null
406
- },
407
-
408
- buildEnd(err) {
409
- if (err) {
410
- globalHasError = true
411
- }
412
- },
413
-
414
- closeBundle() {
415
- // 延迟检测是否是最后一次 closeBundle
416
- setTimeout(() => {
417
- if (!globalIsBuilding) return
418
-
419
- if (globalStallTimer) {
420
- clearInterval(globalStallTimer)
421
- globalStallTimer = null
422
- }
423
-
424
- const elapsed = ((Date.now() - globalStartTime) / 1000).toFixed(1)
425
-
426
- // 显示 100%,带换行
427
- // 100% 时应该显示总数/总数,而不是实际计数/总数
428
- const totalTransforms = hasCache
429
- ? cache.transformCount * 2
430
- : globalTransformCount
431
- const totalChunks = hasCache ? cache.chunkCount * 2 : globalChunkCount
432
- const transformsStr = `${totalTransforms}/${totalTransforms}`
433
- const chunksStr = `${totalChunks}/${totalChunks}`
434
-
435
- renderProgress(
436
- 1,
437
- transformsStr,
438
- chunksStr,
439
- width,
440
- complete,
441
- incomplete,
442
- true,
443
- )
444
-
445
- restoreOutput()
446
-
447
- if (!globalHasError) {
448
- const totalTime = Date.now() - globalStartTime
449
- const transformTime = globalTransformEndTime
450
- ? globalTransformEndTime - globalStartTime
451
- : totalTime
452
- setCacheData({
453
- transformCount: Math.floor(globalTransformCount / 2),
454
- chunkCount: Math.floor(globalChunkCount / 2),
455
- transformEndRatio: transformTime / totalTime,
456
- })
457
-
458
- console.log(`🔨 Rollup 打包完成`)
459
- console.log(` 📁 输出目录: ${globalOutDir}`)
460
- console.log(` ⏱️ 耗时: ${elapsed}s`)
461
- console.log(` ⏳ VitePress 正在渲染页面...`)
462
- } else {
463
- console.log(`\n❌ 构建失败,请检查错误信息`)
464
- }
465
-
466
- globalIsBuilding = false
467
- }, 500)
468
- },
469
- }
470
- }
1
+ /**
2
+ * .vitepress/tnotes/vitepress/plugins/buildProgressPlugin.ts
3
+ *
4
+ * 构建进度插件 - 仅在 build 模式下显示真实进度
5
+ *
6
+ * 基于 vite-plugin-progress 源码简化实现
7
+ * https://github.com/jeddygong/vite-plugin-progress
8
+ */
9
+
10
+ import type { Plugin } from 'vite'
11
+ import {
12
+ existsSync,
13
+ readFileSync,
14
+ writeFileSync,
15
+ mkdirSync,
16
+ readdirSync,
17
+ } from 'fs'
18
+ import { join } from 'path'
19
+
20
+ /** 缓存目录和文件路径 */
21
+ const CACHE_DIR = join(process.cwd(), 'node_modules', '.tnotes-progress')
22
+ const CACHE_FILE = join(CACHE_DIR, 'build-cache.json')
23
+
24
+ /** 缓存数据结构 */
25
+ interface CacheData {
26
+ transformCount: number
27
+ chunkCount: number
28
+ /** transform 阶段耗时占总构建时间的比例(0-1),用于下次按时间权重分配进度 */
29
+ transformEndRatio?: number
30
+ }
31
+
32
+ /** 插件配置选项 */
33
+ interface BuildProgressOptions {
34
+ width?: number
35
+ complete?: string
36
+ incomplete?: string
37
+ }
38
+
39
+ /**
40
+ * 读取缓存数据
41
+ */
42
+ function getCacheData(): CacheData {
43
+ try {
44
+ if (existsSync(CACHE_FILE)) {
45
+ return JSON.parse(readFileSync(CACHE_FILE, 'utf-8'))
46
+ }
47
+ } catch {
48
+ // 忽略错误
49
+ }
50
+ return { transformCount: 0, chunkCount: 0 }
51
+ }
52
+
53
+ /**
54
+ * 写入缓存数据
55
+ */
56
+ function setCacheData(data: CacheData): void {
57
+ try {
58
+ if (!existsSync(CACHE_DIR)) {
59
+ mkdirSync(CACHE_DIR, { recursive: true })
60
+ }
61
+ writeFileSync(CACHE_FILE, JSON.stringify(data), 'utf-8')
62
+ } catch {
63
+ // 忽略错误
64
+ }
65
+ }
66
+
67
+ /**
68
+ * 扫描源目录文件数量
69
+ */
70
+ function countSourceFiles(srcDir: string): number {
71
+ let count = 0
72
+ const extensions = /\.(vue|ts|js|jsx|tsx|css|scss|sass|styl|less|md)$/i
73
+
74
+ const scan = (dir: string) => {
75
+ try {
76
+ const entries = readdirSync(dir, { withFileTypes: true })
77
+ for (const entry of entries) {
78
+ const fullPath = join(dir, entry.name)
79
+ if (
80
+ entry.isDirectory() &&
81
+ !entry.name.startsWith('.') &&
82
+ entry.name !== 'node_modules'
83
+ ) {
84
+ scan(fullPath)
85
+ } else if (entry.isFile() && extensions.test(entry.name)) {
86
+ count++
87
+ }
88
+ }
89
+ } catch {
90
+ // 忽略错误
91
+ }
92
+ }
93
+
94
+ scan(srcDir)
95
+ return count
96
+ }
97
+
98
+ // ============ 全局状态 ============
99
+ let globalStartTime = 0
100
+ let globalTransformCount = 0
101
+ let globalChunkCount = 0
102
+ let globalHasError = false
103
+ let globalIsBuilding = false
104
+ let globalOutDir = ''
105
+ let globalLastPercent = 0
106
+ let globalFileCount = 0
107
+ let globalLastLoggedPercent = -1 // 非 TTY 环境下上次输出的百分比区间,初始为 -1 以便第一次输出
108
+ let globalLastOutputTime = 0 // 非 TTY 环境下上次输出时间戳,用于时间节流
109
+ let globalTransformEndTime = 0 // transform 阶段结束时间戳
110
+ let globalLastHookTime = 0 // 最后一次 Vite hook 触发时间
111
+ let globalStallTimer: ReturnType<typeof setInterval> | null = null
112
+
113
+ /** 检测是否支持单行刷新(交互式终端) */
114
+ const isTTY = !!(process.stdout.isTTY && process.stderr.isTTY)
115
+
116
+ /** 检测是否在 CI 环境中运行 */
117
+ const isCI = !!(
118
+ process.env.CI ||
119
+ process.env.GITHUB_ACTIONS ||
120
+ process.env.GITLAB_CI ||
121
+ process.env.CIRCLECI ||
122
+ process.env.TRAVIS ||
123
+ process.env.JENKINS_URL
124
+ )
125
+
126
+ /** 是否使用单行刷新模式(TTY 且非 CI) */
127
+ const useSingleLineMode = isTTY && !isCI
128
+
129
+ // 保存原始输出函数
130
+ let originalStdoutWrite: typeof process.stdout.write | null = null
131
+ let originalStderrWrite: typeof process.stderr.write | null = null
132
+
133
+ /**
134
+ * 拦截输出
135
+ */
136
+ function interceptOutput() {
137
+ if (originalStdoutWrite) return
138
+
139
+ originalStdoutWrite = process.stdout.write.bind(process.stdout)
140
+ originalStderrWrite = process.stderr.write.bind(process.stderr)
141
+
142
+ const filter = (chunk: string | Uint8Array): boolean => {
143
+ const str = chunk.toString()
144
+ // 只允许我们的输出
145
+ return (
146
+ str.includes('🔨') ||
147
+ str.includes('✅ 构建成功') ||
148
+ str.includes('❌ 构建失败') ||
149
+ str.includes('📁') ||
150
+ str.includes('📊') ||
151
+ str.includes('📦') ||
152
+ str.includes('⏱️') ||
153
+ str.includes('\x1b[2K') // 允许清屏指令
154
+ )
155
+ }
156
+
157
+ process.stdout.write = ((
158
+ chunk: string | Uint8Array,
159
+ encodingOrCallback?: BufferEncoding | ((err?: Error | null) => void),
160
+ callback?: (err?: Error | null) => void,
161
+ ): boolean => {
162
+ if (filter(chunk)) {
163
+ return originalStdoutWrite!(chunk, encodingOrCallback as any, callback)
164
+ }
165
+ if (typeof encodingOrCallback === 'function') encodingOrCallback()
166
+ else if (callback) callback()
167
+ return true
168
+ }) as typeof process.stdout.write
169
+
170
+ process.stderr.write = ((
171
+ chunk: string | Uint8Array,
172
+ encodingOrCallback?: BufferEncoding | ((err?: Error | null) => void),
173
+ callback?: (err?: Error | null) => void,
174
+ ): boolean => {
175
+ const str = chunk.toString()
176
+ // 允许进度条和真正的错误
177
+ if (filter(chunk) || str.toLowerCase().includes('error')) {
178
+ return originalStderrWrite!(chunk, encodingOrCallback as any, callback)
179
+ }
180
+ if (typeof encodingOrCallback === 'function') encodingOrCallback()
181
+ else if (callback) callback()
182
+ return true
183
+ }) as typeof process.stderr.write
184
+ }
185
+
186
+ /**
187
+ * 恢复输出
188
+ */
189
+ function restoreOutput() {
190
+ if (originalStdoutWrite) {
191
+ process.stdout.write = originalStdoutWrite
192
+ originalStdoutWrite = null
193
+ }
194
+ if (originalStderrWrite) {
195
+ process.stderr.write = originalStderrWrite
196
+ originalStderrWrite = null
197
+ }
198
+ }
199
+
200
+ /**
201
+ * 渲染进度条
202
+ */
203
+ function renderProgress(
204
+ percent: number,
205
+ transforms: string,
206
+ chunks: string,
207
+ width: number,
208
+ complete: string,
209
+ incomplete: string,
210
+ isFinal: boolean = false,
211
+ ) {
212
+ if (!originalStderrWrite) return
213
+
214
+ // 非单行模式下,只在 10% 间隔输出进度,并附加时间节流避免多行显示相同耗时
215
+ if (!useSingleLineMode && !isFinal) {
216
+ const currentPercent = Math.floor(percent * 100)
217
+ const currentBucket = Math.floor(currentPercent / 10) * 10
218
+ const now = Date.now()
219
+ if (
220
+ currentBucket <= globalLastLoggedPercent ||
221
+ now - globalLastOutputTime < 500
222
+ ) {
223
+ return
224
+ }
225
+ globalLastLoggedPercent = currentBucket
226
+ globalLastOutputTime = now
227
+ }
228
+
229
+ const filled = Math.floor(percent * width)
230
+ const empty = width - filled
231
+ const bar = complete.repeat(filled) + incomplete.repeat(empty)
232
+ const percentStr = (percent * 100).toFixed(0).padStart(3, ' ')
233
+ const elapsed = ((Date.now() - globalStartTime) / 1000).toFixed(1)
234
+
235
+ // 格式: Building [...] 100% | Transforms: x/y | Chunks: x/y | Time: xs
236
+ // 单行模式使用回车覆盖,否则直接换行
237
+ const prefix = useSingleLineMode ? '\r\x1b[2K' : ''
238
+ const ending = isFinal || !useSingleLineMode ? '\n' : ''
239
+ const line = `${prefix}Building [${bar}] ${percentStr}% | Transforms: ${transforms} | Chunks: ${chunks} | Time: ${elapsed}s${ending}`
240
+ originalStderrWrite(line)
241
+ }
242
+
243
+ /**
244
+ * 创建构建进度插件
245
+ */
246
+ export function buildProgressPlugin(
247
+ options: BuildProgressOptions = {},
248
+ ): Plugin {
249
+ const { width = 40, complete = '█', incomplete = '░' } = options
250
+
251
+ const cache = getCacheData()
252
+ const hasCache = cache.transformCount > 0
253
+
254
+ return {
255
+ name: 'tnotes-build-progress',
256
+ enforce: 'pre',
257
+ apply: 'build',
258
+
259
+ config(config, { command }) {
260
+ if (command === 'build') {
261
+ config.logLevel = 'silent'
262
+
263
+ if (!globalIsBuilding) {
264
+ globalIsBuilding = true
265
+ globalStartTime = Date.now()
266
+ globalTransformCount = 0
267
+ globalChunkCount = 0
268
+ globalHasError = false
269
+ globalLastPercent = 0
270
+ globalLastLoggedPercent = -1
271
+ globalLastOutputTime = 0
272
+ globalTransformEndTime = 0
273
+ globalLastHookTime = Date.now()
274
+ globalOutDir = config.build?.outDir || 'dist'
275
+
276
+ if (!hasCache) {
277
+ globalFileCount = countSourceFiles(process.cwd())
278
+ }
279
+
280
+ interceptOutput()
281
+
282
+ // 停滞填充计时器:当 Vite hook 超过 2 秒没触发时(如 VitePress 渲染页面阶段),
283
+ // 缓慢推进进度条,避免长时间卡住不动
284
+ globalStallTimer = setInterval(() => {
285
+ if (
286
+ !globalIsBuilding ||
287
+ globalLastPercent <= 0 ||
288
+ globalLastPercent >= 0.98
289
+ )
290
+ return
291
+ const now = Date.now()
292
+ if (now - globalLastHookTime < 2000) return
293
+
294
+ // 每次推进剩余距离的 2%,形成自然减速曲线
295
+ const remaining = 0.98 - globalLastPercent
296
+ globalLastPercent += remaining * 0.02
297
+
298
+ const transformsStr = hasCache
299
+ ? `${globalTransformCount}/${cache.transformCount * 2}`
300
+ : `${globalTransformCount}`
301
+ const chunksStr = hasCache
302
+ ? `${globalChunkCount}/${cache.chunkCount * 2}`
303
+ : `${globalChunkCount}`
304
+
305
+ renderProgress(
306
+ globalLastPercent,
307
+ transformsStr,
308
+ chunksStr,
309
+ width,
310
+ complete,
311
+ incomplete,
312
+ )
313
+ }, 1000)
314
+ }
315
+ }
316
+ },
317
+
318
+ transform(_code, _id) {
319
+ globalTransformCount++
320
+ globalLastHookTime = Date.now()
321
+
322
+ // 根据缓存的时间比例确定 transform 阶段在进度条中的权重
323
+ // 例如 transform 占总时间 37% → 进度条 0-36%,chunk 占 63% → 进度条 36-98%
324
+ const transformWeight =
325
+ hasCache && cache.transformEndRatio != null
326
+ ? Math.max(0.1, Math.min(0.85, cache.transformEndRatio * 0.98))
327
+ : 0.85
328
+
329
+ if (hasCache) {
330
+ const totalTransforms = cache.transformCount * 2
331
+ globalLastPercent = Math.min(
332
+ transformWeight,
333
+ (globalTransformCount / totalTransforms) * transformWeight,
334
+ )
335
+ } else {
336
+ // 无缓存模式:渐近曲线平滑逼近上限
337
+ const k = 500
338
+ globalLastPercent = Math.min(
339
+ transformWeight,
340
+ (transformWeight * globalTransformCount) / (globalTransformCount + k),
341
+ )
342
+ }
343
+
344
+ const transformsStr = hasCache
345
+ ? `${globalTransformCount}/${cache.transformCount * 2}`
346
+ : `${globalTransformCount}`
347
+ const chunksStr = hasCache
348
+ ? `${globalChunkCount}/${cache.chunkCount * 2}`
349
+ : `${globalChunkCount}`
350
+
351
+ renderProgress(
352
+ globalLastPercent,
353
+ transformsStr,
354
+ chunksStr,
355
+ width,
356
+ complete,
357
+ incomplete,
358
+ )
359
+
360
+ return null
361
+ },
362
+
363
+ renderChunk() {
364
+ globalChunkCount++
365
+ globalLastHookTime = Date.now()
366
+
367
+ // 记录 transform 阶段结束时间(第一次进入 renderChunk)
368
+ if (!globalTransformEndTime) {
369
+ globalTransformEndTime = Date.now()
370
+ }
371
+
372
+ const transformWeight =
373
+ hasCache && cache.transformEndRatio != null
374
+ ? Math.max(0.1, Math.min(0.85, cache.transformEndRatio * 0.98))
375
+ : 0.85
376
+ const chunkWeight = 0.98 - transformWeight
377
+
378
+ if (hasCache) {
379
+ const totalChunks = cache.chunkCount * 2
380
+ globalLastPercent = Math.min(
381
+ 0.98,
382
+ transformWeight + (globalChunkCount / totalChunks) * chunkWeight,
383
+ )
384
+ } else {
385
+ globalLastPercent = Math.max(globalLastPercent, transformWeight)
386
+ globalLastPercent = Math.min(0.98, globalLastPercent + 0.00005)
387
+ }
388
+
389
+ const transformsStr = hasCache
390
+ ? `${globalTransformCount}/${cache.transformCount * 2}`
391
+ : `${globalTransformCount}`
392
+ const chunksStr = hasCache
393
+ ? `${globalChunkCount}/${cache.chunkCount * 2}`
394
+ : `${globalChunkCount}`
395
+
396
+ renderProgress(
397
+ globalLastPercent,
398
+ transformsStr,
399
+ chunksStr,
400
+ width,
401
+ complete,
402
+ incomplete,
403
+ )
404
+
405
+ return null
406
+ },
407
+
408
+ buildEnd(err) {
409
+ if (err) {
410
+ globalHasError = true
411
+ }
412
+ },
413
+
414
+ closeBundle() {
415
+ // 延迟检测是否是最后一次 closeBundle
416
+ setTimeout(() => {
417
+ if (!globalIsBuilding) return
418
+
419
+ if (globalStallTimer) {
420
+ clearInterval(globalStallTimer)
421
+ globalStallTimer = null
422
+ }
423
+
424
+ const elapsed = ((Date.now() - globalStartTime) / 1000).toFixed(1)
425
+
426
+ // 显示 100%,带换行
427
+ // 100% 时应该显示总数/总数,而不是实际计数/总数
428
+ const totalTransforms = hasCache
429
+ ? cache.transformCount * 2
430
+ : globalTransformCount
431
+ const totalChunks = hasCache ? cache.chunkCount * 2 : globalChunkCount
432
+ const transformsStr = `${totalTransforms}/${totalTransforms}`
433
+ const chunksStr = `${totalChunks}/${totalChunks}`
434
+
435
+ renderProgress(
436
+ 1,
437
+ transformsStr,
438
+ chunksStr,
439
+ width,
440
+ complete,
441
+ incomplete,
442
+ true,
443
+ )
444
+
445
+ restoreOutput()
446
+
447
+ if (!globalHasError) {
448
+ const totalTime = Date.now() - globalStartTime
449
+ const transformTime = globalTransformEndTime
450
+ ? globalTransformEndTime - globalStartTime
451
+ : totalTime
452
+ setCacheData({
453
+ transformCount: Math.floor(globalTransformCount / 2),
454
+ chunkCount: Math.floor(globalChunkCount / 2),
455
+ transformEndRatio: transformTime / totalTime,
456
+ })
457
+
458
+ console.log(`🔨 Rollup 打包完成`)
459
+ console.log(` 📁 输出目录: ${globalOutDir}`)
460
+ console.log(` ⏱️ 耗时: ${elapsed}s`)
461
+ console.log(` ⏳ VitePress 正在渲染页面...`)
462
+ } else {
463
+ console.log(`\n❌ 构建失败,请检查错误信息`)
464
+ }
465
+
466
+ globalIsBuilding = false
467
+ }, 500)
468
+ },
469
+ }
470
+ }