@tnotesjs/core 0.0.2 → 0.0.3

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