@tnotesjs/core 0.0.6 → 0.0.8

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 (116) hide show
  1. package/README.md +87 -84
  2. package/dist/{chunk-JDVN2QYW.js → chunk-JUMVJKWV.js} +615 -614
  3. package/dist/cli/index.js +125 -125
  4. package/dist/vitepress/config/index.js +184 -210
  5. package/package.json +15 -3
  6. package/types/config.ts +61 -61
  7. package/types/index.ts +11 -11
  8. package/types/note.ts +33 -33
  9. package/types/shims.d.ts +3 -4
  10. package/vitepress/assets/icons/icon__check.svg +3 -3
  11. package/vitepress/assets/icons/icon__clipboard.svg +7 -7
  12. package/vitepress/assets/icons/icon__close.svg +1 -1
  13. package/vitepress/assets/icons/icon__collapse.svg +1 -1
  14. package/vitepress/assets/icons/icon__confirm.svg +1 -1
  15. package/vitepress/assets/icons/icon__copy.svg +4 -4
  16. package/vitepress/assets/icons/icon__focus.svg +1 -1
  17. package/vitepress/assets/icons/icon__fold.svg +3 -3
  18. package/vitepress/assets/icons/icon__folder.svg +1 -1
  19. package/vitepress/assets/icons/icon__fullscreen.svg +1 -1
  20. package/vitepress/assets/icons/icon__fullscreen_exit.svg +1 -1
  21. package/vitepress/assets/icons/icon__github.svg +3 -3
  22. package/vitepress/assets/icons/icon__mindmap.svg +1 -1
  23. package/vitepress/assets/icons/icon__next.svg +1 -1
  24. package/vitepress/assets/icons/icon__number_gray.svg +1 -1
  25. package/vitepress/assets/icons/icon__number_purple.svg +1 -1
  26. package/vitepress/assets/icons/icon__prev.svg +1 -1
  27. package/vitepress/assets/icons/icon__restore.svg +1 -1
  28. package/vitepress/assets/icons/icon__rotate.svg +4 -4
  29. package/vitepress/assets/icons/icon__search.svg +1 -1
  30. package/vitepress/assets/icons/icon__sidebar_collapsed.svg +1 -1
  31. package/vitepress/assets/icons/icon__sidebar_opened.svg +1 -1
  32. package/vitepress/assets/icons/icon__totop.svg +5 -5
  33. package/vitepress/assets/icons/icon__vscode.svg +5 -5
  34. package/vitepress/assets/icons/icon__zoom_fit.svg +1 -1
  35. package/vitepress/assets/icons/icon__zoom_in.svg +1 -1
  36. package/vitepress/assets/icons/icon__zoom_out.svg +1 -1
  37. package/vitepress/assets/icons/icon__zoom_reset.svg +1 -1
  38. package/vitepress/assets/icons/index.ts +38 -38
  39. package/vitepress/components/BilibiliOutsidePlayer/BilibiliOutsidePlayer.vue +20 -20
  40. package/vitepress/components/CodeBlockFullscreen/CodeBlockFullscreen.vue +373 -372
  41. package/vitepress/components/CodeBlockFullscreen/index.ts +115 -114
  42. package/vitepress/components/CodeBlockFullscreen/styles.css +64 -64
  43. package/vitepress/components/Discussions/Discussions.module.scss +32 -32
  44. package/vitepress/components/Discussions/Discussions.vue +211 -210
  45. package/vitepress/components/EnWordList/EnWordList.module.scss +124 -124
  46. package/vitepress/components/EnWordList/EnWordList.vue +543 -542
  47. package/vitepress/components/EnWordList/RightClickMenu.module.scss +22 -22
  48. package/vitepress/components/EnWordList/RightClickMenu.vue +66 -66
  49. package/vitepress/components/Footprints/Footprints.module.scss +93 -93
  50. package/vitepress/components/Footprints/Footprints.vue +377 -377
  51. package/vitepress/components/Layout/AboutModal.module.scss +233 -233
  52. package/vitepress/components/Layout/AboutModal.vue +105 -105
  53. package/vitepress/components/Layout/AboutPanel.vue +270 -265
  54. package/vitepress/components/Layout/ContentCollapse.vue +603 -602
  55. package/vitepress/components/Layout/CustomSidebar.vue +608 -605
  56. package/vitepress/components/Layout/DocBeforeControls.vue +149 -139
  57. package/vitepress/components/Layout/DocFooter.vue +233 -232
  58. package/vitepress/components/Layout/ImagePreview.module.scss +201 -201
  59. package/vitepress/components/Layout/ImagePreview.vue +281 -280
  60. package/vitepress/components/Layout/Layout.module.scss +661 -661
  61. package/vitepress/components/Layout/Layout.vue +539 -542
  62. package/vitepress/components/Layout/NoteStatus.vue +140 -139
  63. package/vitepress/components/Layout/SidebarItems.vue +267 -262
  64. package/vitepress/components/Layout/SidebarNavBefore.vue +92 -92
  65. package/vitepress/components/Layout/Swiper.vue +167 -167
  66. package/vitepress/components/Layout/ToggleFullContent.module.scss +11 -11
  67. package/vitepress/components/Layout/ToggleFullContent.vue +35 -34
  68. package/vitepress/components/Layout/ToggleSidebar.module.scss +11 -11
  69. package/vitepress/components/Layout/ToggleSidebar.vue +36 -35
  70. package/vitepress/components/Layout/composables/useCollapseControl.ts +88 -88
  71. package/vitepress/components/Layout/composables/useNoteConfig.ts +119 -121
  72. package/vitepress/components/Layout/composables/useNoteSave.ts +173 -172
  73. package/vitepress/components/Layout/composables/useNoteValidation.ts +85 -85
  74. package/vitepress/components/Layout/composables/useRedirect.ts +113 -110
  75. package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +86 -83
  76. package/vitepress/components/Layout/homeReadme.data.ts +124 -124
  77. package/vitepress/components/LoadingPage/LoadingPage.vue +192 -192
  78. package/vitepress/components/MarkMap/MarkMap.module.scss +159 -159
  79. package/vitepress/components/MarkMap/MarkMap.vue +405 -404
  80. package/vitepress/components/Mermaid/Mermaid.module.scss +275 -275
  81. package/vitepress/components/Mermaid/Mermaid.vue +365 -363
  82. package/vitepress/components/NotesTable/NotesTable.module.scss +77 -77
  83. package/vitepress/components/NotesTable/NotesTable.vue +98 -97
  84. package/vitepress/components/NotesTable/README.md +67 -67
  85. package/vitepress/components/Settings/Settings.module.scss +433 -433
  86. package/vitepress/components/Settings/Settings.vue +306 -305
  87. package/vitepress/components/SidebarCard/MindMapView.vue +484 -483
  88. package/vitepress/components/SidebarCard/NotesTrendChart.vue +108 -108
  89. package/vitepress/components/SidebarCard/SidebarCard.vue +946 -948
  90. package/vitepress/components/Tooltip/Tooltip.vue +70 -70
  91. package/vitepress/components/constants.ts +91 -91
  92. package/vitepress/components/notesConfig.data.ts +73 -73
  93. package/vitepress/components/sidebar.data.ts +59 -59
  94. package/vitepress/components/tnotes-config.data.ts +21 -21
  95. package/vitepress/components/utils.ts +26 -26
  96. package/vitepress/config/index.ts +160 -146
  97. package/vitepress/configs/constants.ts +26 -26
  98. package/vitepress/configs/head.config.ts +25 -24
  99. package/vitepress/configs/index.ts +9 -9
  100. package/vitepress/configs/markdown-it.d.ts +23 -20
  101. package/vitepress/configs/markdown.config.ts +366 -365
  102. package/vitepress/configs/theme.config.ts +108 -108
  103. package/vitepress/plugins/buildProgressPlugin.ts +434 -470
  104. package/vitepress/plugins/getNoteByConfigIdPlugin.ts +107 -106
  105. package/vitepress/plugins/renameNotePlugin.ts +60 -59
  106. package/vitepress/plugins/updateConfigPlugin.ts +63 -62
  107. package/vitepress/theme/index.ts +95 -93
  108. package/vitepress/theme/styles/base.scss +50 -50
  109. package/vitepress/theme/styles/components/404.scss +31 -31
  110. package/vitepress/theme/styles/components/collapse.scss +175 -175
  111. package/vitepress/theme/styles/components/markmap.scss +101 -101
  112. package/vitepress/theme/styles/components/swiper.scss +255 -255
  113. package/vitepress/theme/styles/index.scss +25 -25
  114. package/vitepress/theme/styles/layout.scss +62 -62
  115. package/vitepress/theme/styles/utilities.scss +39 -39
  116. package/vitepress/theme/styles/vitepress-override.scss +25 -25
@@ -1,470 +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 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 {
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
+ }