@tnotesjs/core 0.1.21 → 0.1.23

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