css2class 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/API.md +1143 -0
  2. package/CHANGELOG.md +291 -0
  3. package/CONFIG.md +1096 -0
  4. package/CONTRIBUTING.md +571 -0
  5. package/MIGRATION.md +402 -0
  6. package/README.md +634 -0
  7. package/bin/class2css.js +380 -0
  8. package/class2css.config.js +124 -0
  9. package/common.css +3 -0
  10. package/configs/colors.config.js +62 -0
  11. package/configs/layout.config.js +110 -0
  12. package/configs/spacing.config.js +37 -0
  13. package/configs/typography.config.js +41 -0
  14. package/docs/.vitepress/config.mjs +65 -0
  15. package/docs/.vitepress/theme/custom.css +74 -0
  16. package/docs/.vitepress/theme/index.js +7 -0
  17. package/docs/guide/cli.md +97 -0
  18. package/docs/guide/concepts.md +63 -0
  19. package/docs/guide/config-template.md +365 -0
  20. package/docs/guide/config.md +275 -0
  21. package/docs/guide/faq.md +202 -0
  22. package/docs/guide/getting-started.md +83 -0
  23. package/docs/guide/important-and-static.md +67 -0
  24. package/docs/guide/incremental.md +162 -0
  25. package/docs/guide/rules-reference.md +354 -0
  26. package/docs/guide/units.md +57 -0
  27. package/docs/index.md +68 -0
  28. package/package.json +49 -0
  29. package/run.js +90 -0
  30. package/src/README.md +571 -0
  31. package/src/core/CacheManager.js +650 -0
  32. package/src/core/CompatibilityAdapter.js +264 -0
  33. package/src/core/ConfigManager.js +431 -0
  34. package/src/core/ConfigValidator.js +350 -0
  35. package/src/core/EventBus.js +77 -0
  36. package/src/core/FullScanManager.js +430 -0
  37. package/src/core/StateManager.js +631 -0
  38. package/src/docs/DocsServer.js +179 -0
  39. package/src/example.js +106 -0
  40. package/src/generators/DynamicClassGenerator.js +674 -0
  41. package/src/index.js +1046 -0
  42. package/src/parsers/ClassParser.js +572 -0
  43. package/src/parsers/ImportantParser.js +279 -0
  44. package/src/parsers/RegexCompiler.js +200 -0
  45. package/src/utils/ClassChangeTracker.js +366 -0
  46. package/src/utils/ConfigDiagnostics.js +673 -0
  47. package/src/utils/CssFormatter.js +261 -0
  48. package/src/utils/FileUtils.js +230 -0
  49. package/src/utils/Logger.js +150 -0
  50. package/src/utils/Throttle.js +172 -0
  51. package/src/utils/UnitProcessor.js +334 -0
  52. package/src/utils/WxssClassExtractor.js +137 -0
  53. package/src/watchers/ConfigWatcher.js +413 -0
  54. package/src/watchers/FileWatcher.js +133 -0
  55. package/src/writers/FileWriter.js +302 -0
  56. package/src/writers/UnifiedWriter.js +370 -0
  57. package/styles.config.js +250 -0
@@ -0,0 +1,430 @@
1
+ const path = require('path');
2
+ const fs = require('fs').promises;
3
+
4
+ class FullScanManager {
5
+ constructor(eventBus) {
6
+ this.eventBus = eventBus;
7
+ this.classListSet = new Set();
8
+ this.userStaticClassListSet = new Set();
9
+ this.isLocked = false;
10
+ this.scanTime = null;
11
+ this.fileDataMap = new Map(); // 存储每个文件的样式数据
12
+
13
+ // 引用计数:用于防止共享 class 被误删
14
+ this.classRefCount = new Map(); // className -> count
15
+ this.staticRefCount = new Map(); // staticClassName -> count
16
+
17
+ // 基线 class 集合(增量模式下从输出文件加载的 class,不应被删除)
18
+ this.baselineClassSet = new Set();
19
+ this.baselineStaticClassSet = new Set();
20
+
21
+ // 增量模式开关(运行期新增 class 是否自动加入 baseline)
22
+ this.incrementalMode = false;
23
+ }
24
+
25
+ // 全量扫描所有文件
26
+ async performFullScan(watchPath, fileTypes, classParser, cacheManager, preserveBaseline = true) {
27
+ try {
28
+ const watchPaths = Array.isArray(watchPath) ? watchPath : [watchPath];
29
+ this.eventBus.emit('fullScan:started', { watchPath: watchPaths, fileTypes });
30
+
31
+ // 清空现有数据
32
+ if (preserveBaseline) {
33
+ // 保留基线 class
34
+ const baselineClasses = Array.from(this.baselineClassSet);
35
+ const baselineStaticClasses = Array.from(this.baselineStaticClassSet);
36
+
37
+ this.classListSet.clear();
38
+ this.userStaticClassListSet.clear();
39
+ this.fileDataMap.clear();
40
+ this.classRefCount.clear();
41
+ this.staticRefCount.clear();
42
+
43
+ // 恢复基线 class
44
+ for (const cls of baselineClasses) {
45
+ this.classListSet.add(cls);
46
+ this.classRefCount.set(cls, 0);
47
+ }
48
+ for (const cls of baselineStaticClasses) {
49
+ this.userStaticClassListSet.add(cls);
50
+ this.staticRefCount.set(cls, 0);
51
+ }
52
+ } else {
53
+ // 不保留基线,完全清空(用于重建场景)
54
+ this.classListSet.clear();
55
+ this.userStaticClassListSet.clear();
56
+ this.fileDataMap.clear();
57
+ this.classRefCount.clear();
58
+ this.staticRefCount.clear();
59
+ this.baselineClassSet.clear();
60
+ this.baselineStaticClassSet.clear();
61
+ }
62
+
63
+ // 扫描所有文件(支持多目录/多文件)
64
+ const files = await this.scanTargets(watchPaths, fileTypes);
65
+ this.eventBus.emit('fullScan:filesFound', { count: files.length, files });
66
+
67
+ let processedCount = 0;
68
+ const totalFiles = files.length;
69
+
70
+ for (const filePath of files) {
71
+ try {
72
+ const classInfo = await classParser.parseFile(filePath, cacheManager);
73
+ if (classInfo) {
74
+ this.updateFileData(filePath, classInfo);
75
+ }
76
+ processedCount++;
77
+
78
+ // 进度报告
79
+ if (processedCount % 10 === 0 || processedCount === totalFiles) {
80
+ this.eventBus.emit('fullScan:progress', {
81
+ processed: processedCount,
82
+ total: totalFiles,
83
+ percentage: Math.round((processedCount / totalFiles) * 100),
84
+ });
85
+ }
86
+ } catch (error) {
87
+ this.eventBus.emit('fullScan:fileError', { filePath, error: error.message });
88
+ }
89
+ }
90
+
91
+ // 锁定数据
92
+ this.lockData();
93
+
94
+ this.eventBus.emit('fullScan:completed', {
95
+ totalFiles,
96
+ processedFiles: processedCount,
97
+ classCount: this.classListSet.size,
98
+ staticClassCount: this.userStaticClassListSet.size,
99
+ });
100
+
101
+ return {
102
+ success: true,
103
+ classCount: this.classListSet.size,
104
+ staticClassCount: this.userStaticClassListSet.size,
105
+ fileCount: processedCount,
106
+ };
107
+ } catch (error) {
108
+ this.eventBus.emit('fullScan:error', { error: error.message });
109
+ throw error;
110
+ }
111
+ }
112
+
113
+ // 扫描多个入口(目录/文件)获取所有匹配的文件
114
+ async scanTargets(targetPaths, fileTypes) {
115
+ const files = [];
116
+ const seen = new Set();
117
+
118
+ const addFile = (fp) => {
119
+ const norm = path.normalize(fp);
120
+ if (!seen.has(norm)) {
121
+ seen.add(norm);
122
+ files.push(norm);
123
+ }
124
+ };
125
+
126
+ const scanDir = async (dirPath) => {
127
+ try {
128
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
129
+
130
+ for (const entry of entries) {
131
+ const fullPath = path.join(dirPath, entry.name);
132
+
133
+ if (entry.isDirectory()) {
134
+ await scanDir(fullPath);
135
+ } else if (entry.isFile()) {
136
+ const ext = path.extname(entry.name).slice(1).toLowerCase();
137
+ if (fileTypes.includes(ext)) {
138
+ addFile(fullPath);
139
+ }
140
+ }
141
+ }
142
+ } catch (error) {
143
+ this.eventBus.emit('fullScan:dirError', { dirPath, error: error.message });
144
+ }
145
+ };
146
+
147
+ for (const target of targetPaths.filter(Boolean)) {
148
+ try {
149
+ const stat = await fs.stat(target);
150
+ if (stat.isDirectory()) {
151
+ await scanDir(target);
152
+ } else if (stat.isFile()) {
153
+ const ext = path.extname(target).slice(1).toLowerCase();
154
+ if (fileTypes.includes(ext)) {
155
+ addFile(target);
156
+ }
157
+ }
158
+ } catch (error) {
159
+ // 入口不存在/不可访问:记录但不中断
160
+ this.eventBus.emit('fullScan:dirError', { dirPath: target, error: error.message });
161
+ }
162
+ }
163
+
164
+ return files;
165
+ }
166
+
167
+ // 更新单个文件的数据
168
+ updateFileData(filePath, classInfo) {
169
+ // 移除旧的文件数据(使用引用计数)
170
+ if (this.fileDataMap.has(filePath)) {
171
+ const oldData = this.fileDataMap.get(filePath);
172
+
173
+ // 对旧数据的每个 class 减少引用计数
174
+ for (const cls of oldData.classArr) {
175
+ // 如果是基线 class,不删除
176
+ if (this.baselineClassSet.has(cls)) {
177
+ continue;
178
+ }
179
+
180
+ const count = (this.classRefCount.get(cls) || 0) - 1;
181
+ if (count <= 0) {
182
+ this.classRefCount.delete(cls);
183
+ this.classListSet.delete(cls);
184
+ } else {
185
+ this.classRefCount.set(cls, count);
186
+ }
187
+ }
188
+
189
+ for (const cls of oldData.userStaticClassArr) {
190
+ // 如果是基线 class,不删除
191
+ if (this.baselineStaticClassSet.has(cls)) {
192
+ continue;
193
+ }
194
+
195
+ const count = (this.staticRefCount.get(cls) || 0) - 1;
196
+ if (count <= 0) {
197
+ this.staticRefCount.delete(cls);
198
+ this.userStaticClassListSet.delete(cls);
199
+ } else {
200
+ this.staticRefCount.set(cls, count);
201
+ }
202
+ }
203
+ }
204
+
205
+ // 添加新的文件数据(使用引用计数)
206
+ this.fileDataMap.set(filePath, classInfo);
207
+
208
+ for (const cls of classInfo.classArr) {
209
+ const count = (this.classRefCount.get(cls) || 0) + 1;
210
+ this.classRefCount.set(cls, count);
211
+ this.classListSet.add(cls);
212
+
213
+ // 增量模式下,新增的 class 自动加入 baseline(保证只增不删)
214
+ if (this.incrementalMode) {
215
+ this.baselineClassSet.add(cls);
216
+ }
217
+ }
218
+
219
+ for (const cls of classInfo.userStaticClassArr) {
220
+ const count = (this.staticRefCount.get(cls) || 0) + 1;
221
+ this.staticRefCount.set(cls, count);
222
+ this.userStaticClassListSet.add(cls);
223
+
224
+ // 增量模式下,新增的 class 自动加入 baseline(保证只增不删)
225
+ if (this.incrementalMode) {
226
+ this.baselineStaticClassSet.add(cls);
227
+ }
228
+ }
229
+
230
+ this.eventBus.emit('fullScan:fileUpdated', {
231
+ filePath,
232
+ classCount: classInfo.classArr.length,
233
+ staticClassCount: classInfo.userStaticClassArr.length,
234
+ totalClassCount: this.classListSet.size,
235
+ totalStaticClassCount: this.userStaticClassListSet.size,
236
+ });
237
+ }
238
+
239
+ // 移除文件数据(使用引用计数)
240
+ removeFileData(filePath) {
241
+ if (this.fileDataMap.has(filePath)) {
242
+ const data = this.fileDataMap.get(filePath);
243
+
244
+ // 对每个 class 减少引用计数
245
+ for (const cls of data.classArr) {
246
+ // 如果是基线 class,不删除
247
+ if (this.baselineClassSet.has(cls)) {
248
+ continue;
249
+ }
250
+
251
+ const count = (this.classRefCount.get(cls) || 0) - 1;
252
+ if (count <= 0) {
253
+ this.classRefCount.delete(cls);
254
+ this.classListSet.delete(cls);
255
+ } else {
256
+ this.classRefCount.set(cls, count);
257
+ }
258
+ }
259
+
260
+ for (const cls of data.userStaticClassArr) {
261
+ // 如果是基线 class,不删除
262
+ if (this.baselineStaticClassSet.has(cls)) {
263
+ continue;
264
+ }
265
+
266
+ const count = (this.staticRefCount.get(cls) || 0) - 1;
267
+ if (count <= 0) {
268
+ this.staticRefCount.delete(cls);
269
+ this.userStaticClassListSet.delete(cls);
270
+ } else {
271
+ this.staticRefCount.set(cls, count);
272
+ }
273
+ }
274
+
275
+ this.fileDataMap.delete(filePath);
276
+
277
+ this.eventBus.emit('fullScan:fileRemoved', {
278
+ filePath,
279
+ totalClassCount: this.classListSet.size,
280
+ totalStaticClassCount: this.userStaticClassListSet.size,
281
+ });
282
+ }
283
+ }
284
+
285
+ // 锁定数据
286
+ lockData() {
287
+ this.isLocked = true;
288
+ this.scanTime = Date.now();
289
+ this.eventBus.emit('fullScan:dataLocked', {
290
+ scanTime: this.scanTime,
291
+ classCount: this.classListSet.size,
292
+ staticClassCount: this.userStaticClassListSet.size,
293
+ });
294
+ }
295
+
296
+ // 解锁数据
297
+ unlockData() {
298
+ this.isLocked = false;
299
+ this.scanTime = null;
300
+ this.eventBus.emit('fullScan:dataUnlocked');
301
+ }
302
+
303
+ // 获取合并后的完整数据
304
+ getMergedData() {
305
+ return {
306
+ classListSet: new Set(this.classListSet),
307
+ userStaticClassListSet: new Set(this.userStaticClassListSet),
308
+ isLocked: this.isLocked,
309
+ scanTime: this.scanTime,
310
+ fileCount: this.fileDataMap.size,
311
+ };
312
+ }
313
+
314
+ // 添加基线 class(用于增量模式:从输出文件加载已存在的 class)
315
+ addBaselineClasses(classList, staticClassList) {
316
+ for (const cls of classList) {
317
+ this.baselineClassSet.add(cls);
318
+ if (!this.classListSet.has(cls)) {
319
+ this.classListSet.add(cls);
320
+ this.classRefCount.set(cls, 0); // 基线 class 引用计数为 0,但通过 baselineClassSet 保护
321
+ }
322
+ }
323
+
324
+ for (const cls of staticClassList) {
325
+ this.baselineStaticClassSet.add(cls);
326
+ if (!this.userStaticClassListSet.has(cls)) {
327
+ this.userStaticClassListSet.add(cls);
328
+ this.staticRefCount.set(cls, 0); // 基线 class 引用计数为 0,但通过 baselineStaticClassSet 保护
329
+ }
330
+ }
331
+
332
+ this.eventBus.emit('fullScan:baselineAdded', {
333
+ classCount: classList.length,
334
+ staticClassCount: staticClassList.length,
335
+ totalClassCount: this.classListSet.size,
336
+ totalStaticClassCount: this.userStaticClassListSet.size,
337
+ });
338
+ }
339
+
340
+ // 设置增量模式开关(运行期新增 class 是否自动加入 baseline,从而“只增不删”)
341
+ setIncrementalMode(enabled) {
342
+ this.incrementalMode = !!enabled;
343
+ if (this.eventBus && typeof this.eventBus.emit === 'function') {
344
+ this.eventBus.emit('fullScan:incrementalModeChanged', { enabled: this.incrementalMode });
345
+ }
346
+ }
347
+
348
+ // 获取统计信息
349
+ getStats() {
350
+ return {
351
+ isLocked: this.isLocked,
352
+ scanTime: this.scanTime,
353
+ classCount: this.classListSet.size,
354
+ staticClassCount: this.userStaticClassListSet.size,
355
+ fileCount: this.fileDataMap.size,
356
+ timeSinceLastScan: this.scanTime ? Date.now() - this.scanTime : null,
357
+ };
358
+ }
359
+
360
+ // 检查是否需要等待数据锁定
361
+ async waitForDataLock(maxRetries = 5, retryDelay = 200) {
362
+ let retryCount = 0;
363
+
364
+ while (!this.isLocked && retryCount < maxRetries) {
365
+ this.eventBus.emit('fullScan:waitingForLock', { retryCount, maxRetries });
366
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
367
+ retryCount++;
368
+ }
369
+
370
+ return this.isLocked;
371
+ }
372
+
373
+ // 验证数据完整性
374
+ validateData() {
375
+ const errors = [];
376
+ const warnings = [];
377
+
378
+ if (this.classListSet.size === 0 && this.userStaticClassListSet.size === 0) {
379
+ warnings.push('No style classes found in scan data');
380
+ }
381
+
382
+ if (this.fileDataMap.size === 0) {
383
+ errors.push('No files processed in scan data');
384
+ }
385
+
386
+ // 检查数据一致性
387
+ let totalClassCount = 0;
388
+ let totalStaticCount = 0;
389
+
390
+ for (const [filePath, data] of this.fileDataMap.entries()) {
391
+ totalClassCount += data.classArr.length;
392
+ totalStaticCount += data.userStaticClassArr.length;
393
+ }
394
+
395
+ if (totalClassCount !== this.classListSet.size) {
396
+ warnings.push(
397
+ `Class count mismatch: expected ${totalClassCount}, got ${this.classListSet.size}`
398
+ );
399
+ }
400
+
401
+ if (totalStaticCount !== this.userStaticClassListSet.size) {
402
+ warnings.push(
403
+ `Static class count mismatch: expected ${totalStaticCount}, got ${this.userStaticClassListSet.size}`
404
+ );
405
+ }
406
+
407
+ return { errors, warnings, isValid: errors.length === 0 };
408
+ }
409
+
410
+ // 调试信息
411
+ debug() {
412
+ return {
413
+ stats: this.getStats(),
414
+ validation: this.validateData(),
415
+ sampleClasses: Array.from(this.classListSet).slice(0, 10),
416
+ sampleStaticClasses: Array.from(this.userStaticClassListSet).slice(0, 10),
417
+ fileDataMap: Object.fromEntries(
418
+ Array.from(this.fileDataMap.entries()).map(([path, data]) => [
419
+ path,
420
+ {
421
+ classCount: data.classArr.length,
422
+ staticCount: data.userStaticClassArr.length,
423
+ },
424
+ ])
425
+ ),
426
+ };
427
+ }
428
+ }
429
+
430
+ module.exports = FullScanManager;