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.
- package/API.md +1143 -0
- package/CHANGELOG.md +291 -0
- package/CONFIG.md +1096 -0
- package/CONTRIBUTING.md +571 -0
- package/MIGRATION.md +402 -0
- package/README.md +634 -0
- package/bin/class2css.js +380 -0
- package/class2css.config.js +124 -0
- package/common.css +3 -0
- package/configs/colors.config.js +62 -0
- package/configs/layout.config.js +110 -0
- package/configs/spacing.config.js +37 -0
- package/configs/typography.config.js +41 -0
- package/docs/.vitepress/config.mjs +65 -0
- package/docs/.vitepress/theme/custom.css +74 -0
- package/docs/.vitepress/theme/index.js +7 -0
- package/docs/guide/cli.md +97 -0
- package/docs/guide/concepts.md +63 -0
- package/docs/guide/config-template.md +365 -0
- package/docs/guide/config.md +275 -0
- package/docs/guide/faq.md +202 -0
- package/docs/guide/getting-started.md +83 -0
- package/docs/guide/important-and-static.md +67 -0
- package/docs/guide/incremental.md +162 -0
- package/docs/guide/rules-reference.md +354 -0
- package/docs/guide/units.md +57 -0
- package/docs/index.md +68 -0
- package/package.json +49 -0
- package/run.js +90 -0
- package/src/README.md +571 -0
- package/src/core/CacheManager.js +650 -0
- package/src/core/CompatibilityAdapter.js +264 -0
- package/src/core/ConfigManager.js +431 -0
- package/src/core/ConfigValidator.js +350 -0
- package/src/core/EventBus.js +77 -0
- package/src/core/FullScanManager.js +430 -0
- package/src/core/StateManager.js +631 -0
- package/src/docs/DocsServer.js +179 -0
- package/src/example.js +106 -0
- package/src/generators/DynamicClassGenerator.js +674 -0
- package/src/index.js +1046 -0
- package/src/parsers/ClassParser.js +572 -0
- package/src/parsers/ImportantParser.js +279 -0
- package/src/parsers/RegexCompiler.js +200 -0
- package/src/utils/ClassChangeTracker.js +366 -0
- package/src/utils/ConfigDiagnostics.js +673 -0
- package/src/utils/CssFormatter.js +261 -0
- package/src/utils/FileUtils.js +230 -0
- package/src/utils/Logger.js +150 -0
- package/src/utils/Throttle.js +172 -0
- package/src/utils/UnitProcessor.js +334 -0
- package/src/utils/WxssClassExtractor.js +137 -0
- package/src/watchers/ConfigWatcher.js +413 -0
- package/src/watchers/FileWatcher.js +133 -0
- package/src/writers/FileWriter.js +302 -0
- package/src/writers/UnifiedWriter.js +370 -0
- package/styles.config.js +250 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,1046 @@
|
|
|
1
|
+
// 模块化 class2css 主入口文件
|
|
2
|
+
const EventBus = require('./core/EventBus');
|
|
3
|
+
const StateManager = require('./core/StateManager');
|
|
4
|
+
const ConfigManager = require('./core/ConfigManager');
|
|
5
|
+
const CacheManager = require('./core/CacheManager');
|
|
6
|
+
const FullScanManager = require('./core/FullScanManager');
|
|
7
|
+
const Logger = require('./utils/Logger');
|
|
8
|
+
const SmartThrottle = require('./utils/Throttle');
|
|
9
|
+
const FileUtils = require('./utils/FileUtils');
|
|
10
|
+
const RegexCompiler = require('./parsers/RegexCompiler');
|
|
11
|
+
const ImportantParser = require('./parsers/ImportantParser');
|
|
12
|
+
const ClassParser = require('./parsers/ClassParser');
|
|
13
|
+
const DynamicClassGenerator = require('./generators/DynamicClassGenerator');
|
|
14
|
+
const FileWatcher = require('./watchers/FileWatcher');
|
|
15
|
+
const ConfigWatcher = require('./watchers/ConfigWatcher');
|
|
16
|
+
const FileWriter = require('./writers/FileWriter');
|
|
17
|
+
const UnifiedWriter = require('./writers/UnifiedWriter');
|
|
18
|
+
const WxssClassExtractor = require('./utils/WxssClassExtractor');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
|
|
21
|
+
class Class2CSS {
|
|
22
|
+
constructor(options = {}) {
|
|
23
|
+
this.options = options;
|
|
24
|
+
this.isInitialized = false;
|
|
25
|
+
this.isRunning = false;
|
|
26
|
+
|
|
27
|
+
// 构建时间追踪:存储文件路径到开始时间的映射
|
|
28
|
+
this.buildStartTimes = new Map();
|
|
29
|
+
|
|
30
|
+
// appendDelta 模式:追踪已写入的 class(用于判断哪些是新增的)
|
|
31
|
+
this.everWrittenClassSet = new Set();
|
|
32
|
+
this.everWrittenStaticClassSet = new Set();
|
|
33
|
+
|
|
34
|
+
// 解析失败追踪:用于抑制保存期的瞬时失败噪音(连续失败才告警)
|
|
35
|
+
this.parseFailureCounts = new Map(); // filePath -> count
|
|
36
|
+
|
|
37
|
+
// 初始化核心模块
|
|
38
|
+
this.initializeModules();
|
|
39
|
+
|
|
40
|
+
// 绑定事件处理
|
|
41
|
+
this.bindEvents();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 初始化所有模块
|
|
45
|
+
initializeModules() {
|
|
46
|
+
try {
|
|
47
|
+
// 1. 创建事件总线
|
|
48
|
+
this.eventBus = new EventBus();
|
|
49
|
+
|
|
50
|
+
// 2. 创建状态管理器
|
|
51
|
+
this.stateManager = new StateManager(this.eventBus);
|
|
52
|
+
|
|
53
|
+
// 3. 创建配置管理器
|
|
54
|
+
this.configManager = new ConfigManager(this.eventBus, this.options.configPath);
|
|
55
|
+
|
|
56
|
+
// 4. 创建缓存管理器
|
|
57
|
+
this.cacheManager = new CacheManager(this.eventBus, this.options.cacheSize);
|
|
58
|
+
|
|
59
|
+
// 5. 创建日志工具
|
|
60
|
+
this.logger = new Logger(this.eventBus, this.options.logger);
|
|
61
|
+
|
|
62
|
+
// 6. 创建节流工具
|
|
63
|
+
this.throttle = new SmartThrottle(this.eventBus);
|
|
64
|
+
|
|
65
|
+
// 7. 创建文件工具
|
|
66
|
+
this.fileUtils = new FileUtils(this.eventBus);
|
|
67
|
+
|
|
68
|
+
// 设置配置管理器的依赖(用于读取共用CSS)
|
|
69
|
+
this.configManager.setDependencies(this.fileUtils, this.logger);
|
|
70
|
+
|
|
71
|
+
// 8. 创建正则编译器
|
|
72
|
+
this.regexCompiler = new RegexCompiler(this.eventBus, this.configManager.getImportantFlags());
|
|
73
|
+
|
|
74
|
+
// 9. 创建Important解析器
|
|
75
|
+
this.importantParser = new ImportantParser(
|
|
76
|
+
this.eventBus,
|
|
77
|
+
this.configManager.getImportantFlags()
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// 10. 创建类名解析器
|
|
81
|
+
this.classParser = new ClassParser(
|
|
82
|
+
this.eventBus,
|
|
83
|
+
this.regexCompiler,
|
|
84
|
+
this.importantParser,
|
|
85
|
+
this.configManager.getUserStaticClassSet(),
|
|
86
|
+
this.configManager
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// 11. 创建动态类生成器
|
|
90
|
+
this.dynamicClassGenerator = new DynamicClassGenerator(
|
|
91
|
+
this.eventBus,
|
|
92
|
+
this.configManager,
|
|
93
|
+
this.importantParser
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// 12. 创建文件监听器
|
|
97
|
+
this.fileWatcher = new FileWatcher(this.eventBus, this.configManager);
|
|
98
|
+
|
|
99
|
+
// 13. 创建配置文件监听器
|
|
100
|
+
this.configWatcher = new ConfigWatcher(this.eventBus, this.configManager, this.logger);
|
|
101
|
+
|
|
102
|
+
// 14. 创建全量扫描管理器
|
|
103
|
+
this.fullScanManager = new FullScanManager(this.eventBus);
|
|
104
|
+
|
|
105
|
+
// 15. 创建文件写入器
|
|
106
|
+
this.fileWriter = new FileWriter(this.eventBus, this.configManager, this.fileUtils);
|
|
107
|
+
|
|
108
|
+
// 16. 创建统一文件写入器
|
|
109
|
+
this.unifiedWriter = new UnifiedWriter(
|
|
110
|
+
this.eventBus,
|
|
111
|
+
this.configManager,
|
|
112
|
+
this.dynamicClassGenerator
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// 17. 创建 WXSS class 提取器(用于增量模式)
|
|
116
|
+
this.wxssExtractor = new WxssClassExtractor(this.eventBus);
|
|
117
|
+
|
|
118
|
+
// 检查并设置统一文件模式
|
|
119
|
+
const multiFile = this.configManager.getMultiFile();
|
|
120
|
+
const isUnifiedMode = multiFile?.output?.cssOutType === 'uniFile';
|
|
121
|
+
this.stateManager.setUnifiedFileMode(isUnifiedMode);
|
|
122
|
+
|
|
123
|
+
this.isInitialized = true;
|
|
124
|
+
this.logger.info('Class2CSS 模块初始化成功');
|
|
125
|
+
} catch (error) {
|
|
126
|
+
// 如果logger还没有创建,使用console.error
|
|
127
|
+
if (this.logger) {
|
|
128
|
+
this.logger.errorWithContext('模块初始化失败', error);
|
|
129
|
+
} else {
|
|
130
|
+
console.error('Failed to initialize modules:', error);
|
|
131
|
+
}
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 绑定事件处理
|
|
137
|
+
bindEvents() {
|
|
138
|
+
// 配置相关事件
|
|
139
|
+
this.eventBus.on('config:loaded', (config) => {
|
|
140
|
+
this.logger.config('Configuration loaded successfully');
|
|
141
|
+
this.updateParsers();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
this.eventBus.on('config:error', (error) => {
|
|
145
|
+
this.logger.error('配置错误:', error);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// 解析相关事件
|
|
149
|
+
// this.eventBus.on('parser:completed', (stats) => {
|
|
150
|
+
// this.logger.parser(`Parsing completed: ${stats.totalCount} classes found`);
|
|
151
|
+
// });
|
|
152
|
+
|
|
153
|
+
this.eventBus.on('parser:error', (error) => {
|
|
154
|
+
this.logger.error('解析器错误:', error);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// 生成相关事件
|
|
158
|
+
this.eventBus.on('generator:dynamic:completed', (stats) => {
|
|
159
|
+
this.logger.generator(`Dynamic CSS generation completed: ${stats.generatedCount} classes`);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
this.eventBus.on('generator:userBase:completed', (stats) => {
|
|
163
|
+
this.logger.generator(`User base CSS generation completed: ${stats.generatedCount} classes`);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// 缓存相关事件
|
|
167
|
+
this.eventBus.on('cache:file:updated', (filePath) => {
|
|
168
|
+
this.logger.cache(`File cache updated: ${filePath}`);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
this.eventBus.on('cache:fullScan:updated', (cache) => {
|
|
172
|
+
this.logger.cache(`Full scan cache updated: ${cache.classListSet.size} classes`);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// 文件监听相关事件
|
|
176
|
+
// this.eventBus.on('watcher:ready', (data) => {
|
|
177
|
+
// this.logger.info(`文件监听器就绪: ${data.path}`);
|
|
178
|
+
// });
|
|
179
|
+
|
|
180
|
+
this.eventBus.on('file:changed', (filePath) => {
|
|
181
|
+
// this.logger.info(`文件已更改: ${filePath}`);
|
|
182
|
+
this.handleFileChange(filePath);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
this.eventBus.on('file:added', (filePath) => {
|
|
186
|
+
// this.logger.info(`文件已添加: ${filePath}`);
|
|
187
|
+
this.handleFileChange(filePath);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
this.eventBus.on('file:removed', (filePath) => {
|
|
191
|
+
this.logger.info(`文件已删除: ${filePath}`);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// 文件写入相关事件
|
|
195
|
+
this.eventBus.on('file:css:written', (data) => {
|
|
196
|
+
this.logger.info(`CSS 已写入: ${data.outputFile} (${data.cssLength} 字符)`);
|
|
197
|
+
|
|
198
|
+
// 计算并记录构建时间(统一文件模式跳过,由 unifiedWriter:completed 处理)
|
|
199
|
+
if (data.sourceFile !== 'unified-output') {
|
|
200
|
+
this.logBuildTime(data.sourceFile, data.outputFile);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
this.eventBus.on('file:css:write:error', (data) => {
|
|
205
|
+
this.logger.error(`CSS 写入错误 ${data.sourceFile}: ${data.error}`);
|
|
206
|
+
// 清除构建时间记录
|
|
207
|
+
this.buildStartTimes.delete(data.sourceFile);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// 统一文件写入完成事件
|
|
211
|
+
this.eventBus.on('unifiedWriter:completed', (data) => {
|
|
212
|
+
// 统一文件模式:计算所有待处理文件的构建时间
|
|
213
|
+
if (data.processedFiles && data.processedFiles.length > 0) {
|
|
214
|
+
const buildTime = this.calculateBuildTimeForUnified(data.processedFiles);
|
|
215
|
+
if (buildTime !== null) {
|
|
216
|
+
this.logger.info(`📦 统一文件构建完成: ${data.cssLength} 字符, ${data.classCount} 个类, 耗时 ${buildTime}ms`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// 统一文件写入错误事件
|
|
222
|
+
this.eventBus.on('unifiedWriter:error', (data) => {
|
|
223
|
+
// 清除所有待处理文件的构建时间记录
|
|
224
|
+
if (data.pendingWrites && data.pendingWrites.length > 0) {
|
|
225
|
+
for (const filePath of data.pendingWrites) {
|
|
226
|
+
this.buildStartTimes.delete(filePath);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// 配置监听相关事件
|
|
232
|
+
// this.eventBus.on('config:watcher:ready', (data) => {
|
|
233
|
+
// this.logger.info(`配置监听器就绪: ${data.configPath}`);
|
|
234
|
+
// });
|
|
235
|
+
|
|
236
|
+
this.eventBus.on('config:reload:start', (data) => {
|
|
237
|
+
this.logger.info(`配置重载开始 (第${data.reloadCount}次)`);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
this.eventBus.on('config:reload:success', (data) => {
|
|
241
|
+
this.logger.info(`配置重载成功: 检测到 ${data.changes.length} 个变更`);
|
|
242
|
+
this.handleConfigReload(data);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
this.eventBus.on('config:reload:error', (data) => {
|
|
246
|
+
this.logger.error(`配置重载失败: ${data.error}`);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
this.eventBus.on('config:file:deleted', (data) => {
|
|
250
|
+
this.logger.warn(`配置文件已删除: ${data.filePath}`);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// 错误处理
|
|
254
|
+
this.eventBus.on('log:error', (data) => {
|
|
255
|
+
console.error('Error occurred:', data);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// 更新解析器配置
|
|
260
|
+
updateParsers() {
|
|
261
|
+
try {
|
|
262
|
+
// 更新正则编译器
|
|
263
|
+
this.regexCompiler.updateImportantFlags(this.configManager.getImportantFlags());
|
|
264
|
+
|
|
265
|
+
// 更新Important解析器
|
|
266
|
+
this.importantParser.updateImportantFlags(this.configManager.getImportantFlags());
|
|
267
|
+
|
|
268
|
+
// 更新类名解析器
|
|
269
|
+
this.classParser.updateUserStaticClassSet(this.configManager.getUserStaticClassSet());
|
|
270
|
+
|
|
271
|
+
this.logger.info('解析器已使用新配置更新');
|
|
272
|
+
} catch (error) {
|
|
273
|
+
this.logger.error('更新解析器失败:', error);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// 处理配置重载
|
|
278
|
+
async handleConfigReload(data) {
|
|
279
|
+
try {
|
|
280
|
+
this.logger.info('正在处理配置重载...');
|
|
281
|
+
|
|
282
|
+
// 更新解析器配置
|
|
283
|
+
this.updateParsers();
|
|
284
|
+
|
|
285
|
+
// 检查是否需要重新设置文件监听
|
|
286
|
+
const watchPathChanged = data.changes.some((change) => change.type === 'watchPath');
|
|
287
|
+
if (watchPathChanged) {
|
|
288
|
+
this.logger.info('监听路径已更改,正在重启文件监听器...');
|
|
289
|
+
this.fileWatcher.stopWatching();
|
|
290
|
+
await this.fileWatcher.startWatching();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 检查是否需要重新设置输出模式
|
|
294
|
+
const outputTypeChanged = data.changes.some((change) => change.type === 'outputType');
|
|
295
|
+
if (outputTypeChanged) {
|
|
296
|
+
const config = this.configManager.getConfig();
|
|
297
|
+
this.stateManager.setUnifiedFileMode(config.multiFile.output.cssOutType === 'uniFile');
|
|
298
|
+
this.logger.info(`输出模式已更改为: ${config.multiFile.output.cssOutType}`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 检查是否需要重新生成CSS
|
|
302
|
+
const needsRegeneration = data.changes.some((change) =>
|
|
303
|
+
['baseClassName', 'outputType', 'outputPath'].includes(change.type)
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
if (needsRegeneration) {
|
|
307
|
+
this.logger.info('配置变更需要重新生成CSS');
|
|
308
|
+
await this.performFullScan();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
this.logger.info('配置重载完成');
|
|
312
|
+
} catch (error) {
|
|
313
|
+
this.logger.errorWithContext('处理配置重载失败', error);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// 启动Class2CSS
|
|
318
|
+
async start() {
|
|
319
|
+
if (!this.isInitialized) {
|
|
320
|
+
throw new Error('Class2CSS not initialized');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (this.isRunning) {
|
|
324
|
+
this.logger.warn('Class2CSS 已在运行中');
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
this.isRunning = true;
|
|
330
|
+
this.logger.info('正在启动 Class2CSS...');
|
|
331
|
+
|
|
332
|
+
// 验证配置
|
|
333
|
+
const configErrors = this.configManager.validateConfig();
|
|
334
|
+
if (configErrors.length > 0) {
|
|
335
|
+
throw new Error(`Configuration validation failed: ${configErrors.join(', ')}`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const multiFile = this.configManager.getMultiFile();
|
|
339
|
+
const isIncrementalMode = this.stateManager.isInUnifiedFileMode() && multiFile?.output?.incrementalOnlyAdd;
|
|
340
|
+
const rebuildOnStart = multiFile?.output?.rebuildOnStart !== false; // 默认 true
|
|
341
|
+
const uniFileWriteMode = multiFile?.output?.uniFileWriteMode || 'rewrite';
|
|
342
|
+
|
|
343
|
+
// 如果是 appendDelta 模式
|
|
344
|
+
if (this.stateManager.isInUnifiedFileMode() && uniFileWriteMode === 'appendDelta') {
|
|
345
|
+
// appendDelta 模式要求 rebuildOnStart=true
|
|
346
|
+
if (!rebuildOnStart) {
|
|
347
|
+
throw new Error('uniFileWriteMode="appendDelta" requires rebuildOnStart=true');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// 1. 读取旧输出文件,提取 oldBaselineSet(用于后续 unused 提示)
|
|
351
|
+
let oldBaselineClassSet = new Set();
|
|
352
|
+
let oldBaselineStaticSet = new Set();
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
const outputPath = multiFile.output.path;
|
|
356
|
+
const fileName = multiFile.output.fileName || 'index.wxss';
|
|
357
|
+
const outputFilePath = path.join(outputPath, fileName);
|
|
358
|
+
|
|
359
|
+
this.logger.info(`appendDelta 模式启动重建: 正在读取旧输出文件 ${outputFilePath}`);
|
|
360
|
+
const { classList, staticClassList } = await this.wxssExtractor.extractClassesFromFile(
|
|
361
|
+
outputFilePath
|
|
362
|
+
);
|
|
363
|
+
oldBaselineClassSet = classList;
|
|
364
|
+
oldBaselineStaticSet = staticClassList;
|
|
365
|
+
|
|
366
|
+
if (classList.size > 0 || staticClassList.size > 0) {
|
|
367
|
+
this.logger.info(
|
|
368
|
+
`读取到旧输出文件: ${classList.size} 个动态类, ${staticClassList.size} 个静态类`
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
} catch (error) {
|
|
372
|
+
// 文件不存在或读取失败,继续执行重建(当作首次运行)
|
|
373
|
+
this.logger.info('旧输出文件不存在或读取失败,将执行首次重建');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// 2. 执行全量扫描(不保留旧基线,完全重建)
|
|
377
|
+
const rebuildScanStart = Date.now();
|
|
378
|
+
this.logger.info('执行全量扫描(appendDelta 重建模式)...');
|
|
379
|
+
await this.performFullScan(false); // preserveBaseline = false
|
|
380
|
+
const rebuildScanMs = Date.now() - rebuildScanStart;
|
|
381
|
+
this.logger.info(`appendDelta 启动重建:全量扫描耗时 ${rebuildScanMs}ms`);
|
|
382
|
+
|
|
383
|
+
// 3. 生成 BASE CSS(全量生成,压缩+排序)
|
|
384
|
+
const mergedData = this.fullScanManager.getMergedData();
|
|
385
|
+
const baseGenStart = Date.now();
|
|
386
|
+
const baseCssContent = await this.unifiedWriter.generateUnifiedCSS(
|
|
387
|
+
mergedData.classListSet,
|
|
388
|
+
mergedData.userStaticClassListSet
|
|
389
|
+
);
|
|
390
|
+
const baseGenMs = Date.now() - baseGenStart;
|
|
391
|
+
this.logger.info(`appendDelta 启动重建:BASE CSS 生成耗时 ${baseGenMs}ms`);
|
|
392
|
+
|
|
393
|
+
// 4. 写入 BASE + DELTA_START 标记(覆盖写,清空旧 DELTA)
|
|
394
|
+
this.logger.info('正在写入 BASE 区块和 DELTA_START 标记...');
|
|
395
|
+
const baseWriteStart = Date.now();
|
|
396
|
+
await this.fileWriter.writeBaseWithDeltaMarker(baseCssContent, 'startup-rebuild', {
|
|
397
|
+
forceUniFile: true,
|
|
398
|
+
outputPath: multiFile.output.path,
|
|
399
|
+
fileName: multiFile.output.fileName,
|
|
400
|
+
});
|
|
401
|
+
const baseWriteMs = Date.now() - baseWriteStart;
|
|
402
|
+
this.logger.info(`appendDelta 启动重建:BASE 写入耗时 ${baseWriteMs}ms`);
|
|
403
|
+
|
|
404
|
+
// 5. 记录已写入的 class(用于后续判断新增)
|
|
405
|
+
this.everWrittenClassSet = new Set(mergedData.classListSet);
|
|
406
|
+
this.everWrittenStaticClassSet = new Set(mergedData.userStaticClassListSet);
|
|
407
|
+
|
|
408
|
+
// 6. 计算并打印 unused:oldBaselineSet - scannedSet
|
|
409
|
+
await this.reportUnusedClassesOnRebuild(oldBaselineClassSet, oldBaselineStaticSet);
|
|
410
|
+
|
|
411
|
+
// 7. 初始化运行期 baseline:把当前扫描集合写入 baseline,并开启增量模式
|
|
412
|
+
this.fullScanManager.addBaselineClasses(
|
|
413
|
+
Array.from(mergedData.classListSet),
|
|
414
|
+
Array.from(mergedData.userStaticClassListSet)
|
|
415
|
+
);
|
|
416
|
+
this.fullScanManager.setIncrementalMode(true);
|
|
417
|
+
this.logger.info('appendDelta 模式已启用:运行期将只追加新增 class');
|
|
418
|
+
}
|
|
419
|
+
// 如果是增量模式且开启了启动重建(rewrite 模式)
|
|
420
|
+
else if (isIncrementalMode && rebuildOnStart) {
|
|
421
|
+
// 1. 读取旧输出文件,提取 oldBaselineSet(用于后续 unused 提示)
|
|
422
|
+
let oldBaselineClassSet = new Set();
|
|
423
|
+
let oldBaselineStaticSet = new Set();
|
|
424
|
+
|
|
425
|
+
try {
|
|
426
|
+
const outputPath = multiFile.output.path;
|
|
427
|
+
const fileName = multiFile.output.fileName || 'index.wxss';
|
|
428
|
+
const outputFilePath = path.join(outputPath, fileName);
|
|
429
|
+
|
|
430
|
+
this.logger.info(`增量模式启动重建: 正在读取旧输出文件 ${outputFilePath}`);
|
|
431
|
+
const { classList, staticClassList } = await this.wxssExtractor.extractClassesFromFile(
|
|
432
|
+
outputFilePath
|
|
433
|
+
);
|
|
434
|
+
oldBaselineClassSet = classList;
|
|
435
|
+
oldBaselineStaticSet = staticClassList;
|
|
436
|
+
|
|
437
|
+
if (classList.size > 0 || staticClassList.size > 0) {
|
|
438
|
+
this.logger.info(
|
|
439
|
+
`读取到旧输出文件: ${classList.size} 个动态类, ${staticClassList.size} 个静态类`
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
} catch (error) {
|
|
443
|
+
// 文件不存在或读取失败,继续执行重建(当作首次运行)
|
|
444
|
+
this.logger.info('旧输出文件不存在或读取失败,将执行首次重建');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// 2. 执行全量扫描(不保留旧基线,完全重建)
|
|
448
|
+
this.logger.info('执行全量扫描(重建模式)...');
|
|
449
|
+
await this.performFullScan(false); // preserveBaseline = false
|
|
450
|
+
|
|
451
|
+
// 3. 立即写入一次 uniFile(覆盖写,得到干净、排序好的输出)
|
|
452
|
+
this.logger.info('正在写入重建后的输出文件...');
|
|
453
|
+
await this.unifiedWriter.immediateWrite(
|
|
454
|
+
this.fullScanManager,
|
|
455
|
+
this.fileWriter,
|
|
456
|
+
'startup-rebuild'
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
// 4. 计算并打印 unused:oldBaselineSet - scannedSet
|
|
460
|
+
await this.reportUnusedClassesOnRebuild(oldBaselineClassSet, oldBaselineStaticSet);
|
|
461
|
+
|
|
462
|
+
// 5. 初始化运行期 baseline:把当前扫描集合写入 baseline,并开启增量模式
|
|
463
|
+
const mergedData = this.fullScanManager.getMergedData();
|
|
464
|
+
this.fullScanManager.addBaselineClasses(
|
|
465
|
+
Array.from(mergedData.classListSet),
|
|
466
|
+
Array.from(mergedData.userStaticClassListSet)
|
|
467
|
+
);
|
|
468
|
+
this.fullScanManager.setIncrementalMode(true);
|
|
469
|
+
this.logger.info('运行期增量模式已启用(只增不删)');
|
|
470
|
+
} else if (isIncrementalMode && !rebuildOnStart) {
|
|
471
|
+
// 增量模式但 rebuildOnStart=false:按原有逻辑从输出文件加载基线
|
|
472
|
+
await this.loadIncrementalBaseline();
|
|
473
|
+
await this.performFullScan();
|
|
474
|
+
// 初始化运行期 baseline
|
|
475
|
+
const mergedData = this.fullScanManager.getMergedData();
|
|
476
|
+
this.fullScanManager.addBaselineClasses(
|
|
477
|
+
Array.from(mergedData.classListSet),
|
|
478
|
+
Array.from(mergedData.userStaticClassListSet)
|
|
479
|
+
);
|
|
480
|
+
this.fullScanManager.setIncrementalMode(true);
|
|
481
|
+
} else {
|
|
482
|
+
// 标准模式:正常执行全量扫描
|
|
483
|
+
await this.performFullScan();
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// 启动文件监听
|
|
487
|
+
await this.fileWatcher.startWatching();
|
|
488
|
+
|
|
489
|
+
// 启动配置文件监听
|
|
490
|
+
this.configWatcher.startWatching(this.options.configPath || './class2css.config.js');
|
|
491
|
+
|
|
492
|
+
this.logger.info('Class2CSS 启动成功');
|
|
493
|
+
this.eventBus.emit('class2css:started');
|
|
494
|
+
} catch (error) {
|
|
495
|
+
this.isRunning = false;
|
|
496
|
+
this.logger.errorWithContext('启动 Class2CSS 失败', error);
|
|
497
|
+
throw error;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// 停止Class2CSS
|
|
502
|
+
stop() {
|
|
503
|
+
if (!this.isRunning) {
|
|
504
|
+
this.logger.warn('Class2CSS 未在运行');
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
try {
|
|
509
|
+
this.isRunning = false;
|
|
510
|
+
|
|
511
|
+
// 停止文件监听
|
|
512
|
+
this.fileWatcher.stopWatching();
|
|
513
|
+
|
|
514
|
+
// 停止配置文件监听
|
|
515
|
+
this.configWatcher.stopWatching();
|
|
516
|
+
|
|
517
|
+
// 清理资源
|
|
518
|
+
this.throttle.cancelAll();
|
|
519
|
+
this.cacheManager.clearAll();
|
|
520
|
+
|
|
521
|
+
this.logger.info('Class2CSS 已停止');
|
|
522
|
+
this.eventBus.emit('class2css:stopped');
|
|
523
|
+
} catch (error) {
|
|
524
|
+
this.logger.error('停止 Class2CSS 时出错:', error);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// 执行全量扫描
|
|
529
|
+
async performFullScan(preserveBaseline = true) {
|
|
530
|
+
if (this.stateManager.isCurrentlyScanning()) {
|
|
531
|
+
this.logger.scan('Full scan already in progress, skipping');
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
try {
|
|
536
|
+
this.stateManager.setScanning(true);
|
|
537
|
+
this.logger.scan('Starting full scan...');
|
|
538
|
+
|
|
539
|
+
const multiFile = this.configManager.getMultiFile();
|
|
540
|
+
const entryPaths = this.configManager.getMultiFileEntryPaths();
|
|
541
|
+
if (!multiFile || !multiFile.entry || entryPaths.length === 0) {
|
|
542
|
+
throw new Error('MultiFile configuration is required for full scan');
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// 执行真正的全量扫描
|
|
546
|
+
const result = await this.fullScanManager.performFullScan(
|
|
547
|
+
entryPaths,
|
|
548
|
+
multiFile.entry.fileType || ['html', 'wxml'],
|
|
549
|
+
this.classParser,
|
|
550
|
+
this.cacheManager,
|
|
551
|
+
preserveBaseline
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
// 同步状态到StateManager
|
|
555
|
+
this.stateManager.syncWithFullScanManager(this.fullScanManager.getMergedData());
|
|
556
|
+
|
|
557
|
+
this.logger.scan(
|
|
558
|
+
`Full scan completed: ${result.fileCount} files, ${result.classCount} classes, ${result.staticClassCount} static classes`
|
|
559
|
+
);
|
|
560
|
+
this.stateManager.setScanCompleted();
|
|
561
|
+
|
|
562
|
+
// 如果是统一文件模式,执行初始写入(仅在非重建场景,重建场景已在 start() 中处理)
|
|
563
|
+
if (this.stateManager.isInUnifiedFileMode() && preserveBaseline) {
|
|
564
|
+
this.logger.info('检测到统一文件模式,正在执行初始写入...');
|
|
565
|
+
await this.unifiedWriter.immediateWrite(
|
|
566
|
+
this.fullScanManager,
|
|
567
|
+
this.fileWriter,
|
|
568
|
+
'initial-scan'
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
// 如果是增量模式但未开启 rebuildOnStart,检查并报告未使用的 class
|
|
572
|
+
const multiFile = this.configManager.getMultiFile();
|
|
573
|
+
if (multiFile?.output?.incrementalOnlyAdd && !multiFile?.output?.rebuildOnStart) {
|
|
574
|
+
await this.reportUnusedClasses();
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return result;
|
|
579
|
+
} catch (error) {
|
|
580
|
+
this.logger.error('全量扫描失败:', error);
|
|
581
|
+
throw error;
|
|
582
|
+
} finally {
|
|
583
|
+
this.stateManager.setScanning(false);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// 处理文件变更
|
|
588
|
+
async handleFileChange(filePath) {
|
|
589
|
+
if (!this.isRunning) {
|
|
590
|
+
this.logger.warn('Class2CSS 未运行,忽略文件变更');
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
try {
|
|
595
|
+
// 记录构建开始时间
|
|
596
|
+
this.buildStartTimes.set(filePath, Date.now());
|
|
597
|
+
|
|
598
|
+
// this.logger.info(`正在处理文件变更: ${filePath}`);
|
|
599
|
+
|
|
600
|
+
// 解析文件
|
|
601
|
+
let classInfo = null;
|
|
602
|
+
// 保存过程中可能读到空内容/锁定,做轻量重试;若仍失败,延迟再试并抑制噪音
|
|
603
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
604
|
+
classInfo = await this.classParser.parseFile(filePath, this.cacheManager);
|
|
605
|
+
if (classInfo) break;
|
|
606
|
+
await new Promise((resolve) => setTimeout(resolve, 120 * Math.pow(2, attempt)));
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (!classInfo) {
|
|
610
|
+
const nextCount = (this.parseFailureCounts.get(filePath) || 0) + 1;
|
|
611
|
+
this.parseFailureCounts.set(filePath, nextCount);
|
|
612
|
+
|
|
613
|
+
// 用节流做一次“稍后重试”,避免保存风暴期间疯狂刷 warn
|
|
614
|
+
this.throttle.throttle(
|
|
615
|
+
`reparse:${filePath}`,
|
|
616
|
+
() => {
|
|
617
|
+
// 重新触发处理(异步,不阻塞 throttle 回调)
|
|
618
|
+
this.handleFileChange(filePath).catch(() => {});
|
|
619
|
+
},
|
|
620
|
+
800,
|
|
621
|
+
1
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
// 连续失败到一定次数才告警(默认 3 次)
|
|
625
|
+
if (nextCount >= 3) {
|
|
626
|
+
this.logger.warn(`解析文件失败(连续${nextCount}次): ${filePath},已安排重试`);
|
|
627
|
+
}
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// 成功则清零失败计数
|
|
632
|
+
if (this.parseFailureCounts.has(filePath)) {
|
|
633
|
+
this.parseFailureCounts.delete(filePath);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// console.log(
|
|
637
|
+
// `🔍 解析完成: 发现 ${classInfo.classArr.length + classInfo.userStaticClassArr.length} 个类名`
|
|
638
|
+
// );
|
|
639
|
+
|
|
640
|
+
// 根据输出模式选择处理策略
|
|
641
|
+
if (this.stateManager.isInUnifiedFileMode()) {
|
|
642
|
+
const multiFile = this.configManager.getMultiFile();
|
|
643
|
+
const uniFileWriteMode = multiFile?.output?.uniFileWriteMode || 'rewrite';
|
|
644
|
+
|
|
645
|
+
// 更新全量数据
|
|
646
|
+
this.fullScanManager.updateFileData(filePath, classInfo);
|
|
647
|
+
this.stateManager.syncWithFullScanManager(this.fullScanManager.getMergedData());
|
|
648
|
+
|
|
649
|
+
// appendDelta 模式:只追加新增的 class
|
|
650
|
+
if (uniFileWriteMode === 'appendDelta') {
|
|
651
|
+
const mergedData = this.fullScanManager.getMergedData();
|
|
652
|
+
|
|
653
|
+
// 计算新增的 class(当前扫描到的 - 已写入的)
|
|
654
|
+
const newClasses = Array.from(mergedData.classListSet).filter(
|
|
655
|
+
(cls) => !this.everWrittenClassSet.has(cls)
|
|
656
|
+
);
|
|
657
|
+
const newStaticClasses = Array.from(mergedData.userStaticClassListSet).filter(
|
|
658
|
+
(cls) => !this.everWrittenStaticClassSet.has(cls)
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
if (newClasses.length > 0 || newStaticClasses.length > 0) {
|
|
662
|
+
// 生成新增 class 的 CSS
|
|
663
|
+
const deltaGenStart = Date.now();
|
|
664
|
+
const deltaCssContent = await this.generateDeltaCSS(newClasses, newStaticClasses);
|
|
665
|
+
const deltaGenMs = Date.now() - deltaGenStart;
|
|
666
|
+
|
|
667
|
+
if (deltaCssContent.trim()) {
|
|
668
|
+
// 追加到文件末尾
|
|
669
|
+
const appendStart = Date.now();
|
|
670
|
+
await this.fileWriter.appendCSS(deltaCssContent, filePath, {
|
|
671
|
+
forceUniFile: true,
|
|
672
|
+
outputPath: multiFile.output.path,
|
|
673
|
+
fileName: multiFile.output.fileName,
|
|
674
|
+
});
|
|
675
|
+
const appendMs = Date.now() - appendStart;
|
|
676
|
+
|
|
677
|
+
// 记录已写入的 class
|
|
678
|
+
newClasses.forEach((cls) => this.everWrittenClassSet.add(cls));
|
|
679
|
+
newStaticClasses.forEach((cls) => this.everWrittenStaticClassSet.add(cls));
|
|
680
|
+
|
|
681
|
+
// 同时加入 baseline(保证只增不删)
|
|
682
|
+
this.fullScanManager.addBaselineClasses(newClasses, newStaticClasses);
|
|
683
|
+
|
|
684
|
+
// 打印新增动态类名(限制数量,避免刷屏)
|
|
685
|
+
const maxLogClasses = 20;
|
|
686
|
+
const dynamicPreview = newClasses.slice(0, maxLogClasses);
|
|
687
|
+
const dynamicMore = newClasses.length > maxLogClasses ? ` ...(+${newClasses.length - maxLogClasses})` : '';
|
|
688
|
+
|
|
689
|
+
this.logger.info(
|
|
690
|
+
`appendDelta: 追加了 ${newClasses.length} 个动态类, ${newStaticClasses.length} 个静态类(生成 ${deltaGenMs}ms,写入 ${appendMs}ms)` +
|
|
691
|
+
(newClasses.length > 0
|
|
692
|
+
? ` 新增动态类: ${dynamicPreview.join(', ')}${dynamicMore}`
|
|
693
|
+
: '')
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
} else {
|
|
698
|
+
// rewrite 模式:使用防抖写入(全量覆盖)
|
|
699
|
+
this.unifiedWriter.debouncedWrite(this.fullScanManager, this.fileWriter, filePath);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// this.logger.info(`统一文件模式: 已更新 ${filePath} 的数据,触发防抖写入`);
|
|
703
|
+
} else {
|
|
704
|
+
// 单文件模式:使用现有逻辑
|
|
705
|
+
const dynamicResult = this.dynamicClassGenerator.getClassList(classInfo.classArr);
|
|
706
|
+
const userBaseResult = this.dynamicClassGenerator.createUserBaseClassList(
|
|
707
|
+
dynamicResult.userBaseClassArr
|
|
708
|
+
);
|
|
709
|
+
|
|
710
|
+
console.log(
|
|
711
|
+
`🎨 动态CSS生成完成: ${dynamicResult.cssStr.split('\n').filter(Boolean).length} 个类`
|
|
712
|
+
);
|
|
713
|
+
|
|
714
|
+
// 获取共用CSS内容
|
|
715
|
+
const commonCssContent = await this.configManager.getCommonCssContent();
|
|
716
|
+
|
|
717
|
+
// 合并CSS内容(共用CSS前置)
|
|
718
|
+
let cssContent = [commonCssContent, dynamicResult.cssStr, userBaseResult]
|
|
719
|
+
.filter(Boolean)
|
|
720
|
+
.join('\n');
|
|
721
|
+
|
|
722
|
+
// 如果格式为compressed,对整个CSS进行压缩处理
|
|
723
|
+
const cssFormat = this.configManager.getCssFormat();
|
|
724
|
+
const CssFormatter = require('./utils/CssFormatter');
|
|
725
|
+
const cssFormatter = new CssFormatter(cssFormat);
|
|
726
|
+
|
|
727
|
+
// 如果启用了排序,对CSS规则进行字母排序(在格式化之前排序)
|
|
728
|
+
const sortClasses = this.configManager.getSortClasses();
|
|
729
|
+
if (sortClasses) {
|
|
730
|
+
cssContent = cssFormatter.sortCSSRules(cssContent);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// 根据配置的格式对整个CSS进行格式化处理
|
|
734
|
+
cssContent = cssFormatter.formatCSS(cssContent, cssFormat);
|
|
735
|
+
|
|
736
|
+
// 写入CSS文件
|
|
737
|
+
await this.fileWriter.writeCSS(cssContent, filePath);
|
|
738
|
+
|
|
739
|
+
this.logger.info(`CSS 生成完成: ${filePath}`);
|
|
740
|
+
}
|
|
741
|
+
} catch (error) {
|
|
742
|
+
this.logger.errorWithContext(`处理文件变更时出错: ${filePath}`, error);
|
|
743
|
+
// 清除构建时间记录
|
|
744
|
+
this.buildStartTimes.delete(filePath);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// 记录构建时间
|
|
749
|
+
logBuildTime(sourceFile, outputFile) {
|
|
750
|
+
const startTime = this.buildStartTimes.get(sourceFile);
|
|
751
|
+
if (startTime) {
|
|
752
|
+
const buildTime = Date.now() - startTime;
|
|
753
|
+
this.logger.info(`⏱️ 构建完成: ${sourceFile} -> ${outputFile}, 耗时 ${buildTime}ms`);
|
|
754
|
+
this.buildStartTimes.delete(sourceFile);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// 加载增量模式的基线(从输出文件读取已存在的 class)
|
|
759
|
+
async loadIncrementalBaseline() {
|
|
760
|
+
try {
|
|
761
|
+
const multiFile = this.configManager.getMultiFile();
|
|
762
|
+
if (!multiFile || !multiFile.output) {
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const outputPath = multiFile.output.path;
|
|
767
|
+
const fileName = multiFile.output.fileName || 'index.wxss';
|
|
768
|
+
const outputFilePath = path.join(outputPath, fileName);
|
|
769
|
+
|
|
770
|
+
this.logger.info(`正在加载增量基线: ${outputFilePath}`);
|
|
771
|
+
|
|
772
|
+
const { classList, staticClassList } = await this.wxssExtractor.extractClassesFromFile(
|
|
773
|
+
outputFilePath
|
|
774
|
+
);
|
|
775
|
+
|
|
776
|
+
if (classList.size > 0 || staticClassList.size > 0) {
|
|
777
|
+
this.fullScanManager.addBaselineClasses(
|
|
778
|
+
Array.from(classList),
|
|
779
|
+
Array.from(staticClassList)
|
|
780
|
+
);
|
|
781
|
+
this.logger.info(
|
|
782
|
+
`增量基线加载完成: ${classList.size} 个动态类, ${staticClassList.size} 个静态类`
|
|
783
|
+
);
|
|
784
|
+
} else {
|
|
785
|
+
this.logger.info('输出文件不存在或为空,跳过基线加载');
|
|
786
|
+
}
|
|
787
|
+
} catch (error) {
|
|
788
|
+
this.logger.warn(`加载增量基线失败: ${error.message}`);
|
|
789
|
+
// 基线加载失败不影响正常流程
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// 生成增量 CSS(仅生成新增 class 的规则,用于 appendDelta 模式)
|
|
794
|
+
async generateDeltaCSS(newClasses, newStaticClasses) {
|
|
795
|
+
try {
|
|
796
|
+
if ((!newClasses || newClasses.length === 0) && (!newStaticClasses || newStaticClasses.length === 0)) {
|
|
797
|
+
return '';
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// 生成动态CSS(仅新增的 class)
|
|
801
|
+
const dynamicResult = this.dynamicClassGenerator.getClassList(newClasses);
|
|
802
|
+
|
|
803
|
+
// 生成用户基础类CSS(基于新增动态类)
|
|
804
|
+
const userBaseResult = this.dynamicClassGenerator.createUserBaseClassList(
|
|
805
|
+
dynamicResult.userBaseClassArr
|
|
806
|
+
);
|
|
807
|
+
|
|
808
|
+
// 生成静态类CSS(仅新增的静态类)
|
|
809
|
+
const staticResult = await this.unifiedWriter.generateStaticCSS(newStaticClasses);
|
|
810
|
+
|
|
811
|
+
// 合并CSS内容(不包含 commonCss,因为已经在 BASE 中)
|
|
812
|
+
let cssContent = [dynamicResult.cssStr, staticResult, userBaseResult]
|
|
813
|
+
.filter(Boolean)
|
|
814
|
+
.join('\n');
|
|
815
|
+
|
|
816
|
+
// 格式化(压缩),但不排序(因为只是追加)
|
|
817
|
+
const cssFormat = this.configManager.getCssFormat();
|
|
818
|
+
const CssFormatter = require('./utils/CssFormatter');
|
|
819
|
+
const cssFormatter = new CssFormatter(cssFormat);
|
|
820
|
+
cssContent = cssFormatter.formatCSS(cssContent, cssFormat);
|
|
821
|
+
|
|
822
|
+
return cssContent;
|
|
823
|
+
} catch (error) {
|
|
824
|
+
this.logger.error(`生成增量 CSS 失败: ${error.message}`);
|
|
825
|
+
return '';
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// 报告未使用的 class(重建场景:旧输出文件中存在但当前扫描未使用的)
|
|
830
|
+
async reportUnusedClassesOnRebuild(oldBaselineClassSet, oldBaselineStaticSet) {
|
|
831
|
+
try {
|
|
832
|
+
const multiFile = this.configManager.getMultiFile();
|
|
833
|
+
if (!multiFile || !multiFile.output) {
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const unusedReportLimit = multiFile.output.unusedReportLimit || 200;
|
|
838
|
+
|
|
839
|
+
// 获取当前扫描到的 class
|
|
840
|
+
const mergedData = this.fullScanManager.getMergedData();
|
|
841
|
+
const scannedClassSet = mergedData.classListSet;
|
|
842
|
+
const scannedStaticSet = mergedData.userStaticClassListSet;
|
|
843
|
+
|
|
844
|
+
// 计算未使用的 class
|
|
845
|
+
const unusedClasses = Array.from(oldBaselineClassSet).filter(
|
|
846
|
+
(cls) => !scannedClassSet.has(cls)
|
|
847
|
+
);
|
|
848
|
+
const unusedStaticClasses = Array.from(oldBaselineStaticSet).filter(
|
|
849
|
+
(cls) => !scannedStaticSet.has(cls)
|
|
850
|
+
);
|
|
851
|
+
|
|
852
|
+
const totalUnused = unusedClasses.length + unusedStaticClasses.length;
|
|
853
|
+
|
|
854
|
+
if (totalUnused > 0) {
|
|
855
|
+
console.log('\n⚠️ 启动重建:检测到未使用的 class(已从输出文件中清理):');
|
|
856
|
+
console.log(` 总数: ${totalUnused} (动态类: ${unusedClasses.length}, 静态类: ${unusedStaticClasses.length})`);
|
|
857
|
+
|
|
858
|
+
// 显示前 N 个示例
|
|
859
|
+
const displayLimit = Math.min(unusedReportLimit, totalUnused);
|
|
860
|
+
const displayClasses = [
|
|
861
|
+
...unusedClasses.slice(0, Math.min(unusedReportLimit, unusedClasses.length)),
|
|
862
|
+
...unusedStaticClasses.slice(0, Math.min(unusedReportLimit, unusedStaticClasses.length)),
|
|
863
|
+
].slice(0, displayLimit);
|
|
864
|
+
|
|
865
|
+
if (displayClasses.length > 0) {
|
|
866
|
+
console.log(` 示例 (前 ${displayLimit} 个):`);
|
|
867
|
+
displayClasses.forEach((cls, index) => {
|
|
868
|
+
if (index < 20) {
|
|
869
|
+
// 只显示前 20 个,避免输出过长
|
|
870
|
+
console.log(` - ${cls}`);
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
if (displayClasses.length > 20) {
|
|
874
|
+
console.log(` ... 还有 ${displayClasses.length - 20} 个未显示`);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
if (totalUnused > displayLimit) {
|
|
879
|
+
console.log(` (仅显示前 ${displayLimit} 个,实际清理了 ${totalUnused} 个未使用的 class)`);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
console.log(' 提示: 这些 class 在上一版输出文件中存在,但当前项目扫描未使用,已在重建时清理。\n');
|
|
883
|
+
} else {
|
|
884
|
+
console.log('\n✅ 启动重建完成:未发现未使用的 class,输出文件已是最新状态。\n');
|
|
885
|
+
}
|
|
886
|
+
} catch (error) {
|
|
887
|
+
// 报告失败不影响正常流程
|
|
888
|
+
this.logger.warn(`检查未使用 class 失败: ${error.message}`);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// 报告未使用的 class(输出文件中存在但当前扫描未使用的)- 旧版本(用于非重建场景)
|
|
893
|
+
async reportUnusedClasses() {
|
|
894
|
+
try {
|
|
895
|
+
const multiFile = this.configManager.getMultiFile();
|
|
896
|
+
if (!multiFile || !multiFile.output) {
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
const outputPath = multiFile.output.path;
|
|
901
|
+
const fileName = multiFile.output.fileName || 'index.wxss';
|
|
902
|
+
const outputFilePath = path.join(outputPath, fileName);
|
|
903
|
+
const unusedReportLimit = multiFile.output.unusedReportLimit || 200;
|
|
904
|
+
|
|
905
|
+
// 从输出文件提取所有 class
|
|
906
|
+
const { classList: baselineClassList, staticClassList: baselineStaticList } =
|
|
907
|
+
await this.wxssExtractor.extractClassesFromFile(outputFilePath);
|
|
908
|
+
|
|
909
|
+
// 获取当前扫描到的 class
|
|
910
|
+
const mergedData = this.fullScanManager.getMergedData();
|
|
911
|
+
const scannedClassSet = mergedData.classListSet;
|
|
912
|
+
const scannedStaticSet = mergedData.userStaticClassListSet;
|
|
913
|
+
|
|
914
|
+
// 计算未使用的 class
|
|
915
|
+
const unusedClasses = Array.from(baselineClassList).filter(
|
|
916
|
+
(cls) => !scannedClassSet.has(cls)
|
|
917
|
+
);
|
|
918
|
+
const unusedStaticClasses = Array.from(baselineStaticList).filter(
|
|
919
|
+
(cls) => !scannedStaticSet.has(cls)
|
|
920
|
+
);
|
|
921
|
+
|
|
922
|
+
const totalUnused = unusedClasses.length + unusedStaticClasses.length;
|
|
923
|
+
|
|
924
|
+
if (totalUnused > 0) {
|
|
925
|
+
console.log('\n⚠️ 检测到未使用的 class:');
|
|
926
|
+
console.log(` 总数: ${totalUnused} (动态类: ${unusedClasses.length}, 静态类: ${unusedStaticClasses.length})`);
|
|
927
|
+
|
|
928
|
+
// 显示前 N 个示例
|
|
929
|
+
const displayLimit = Math.min(unusedReportLimit, totalUnused);
|
|
930
|
+
const displayClasses = [
|
|
931
|
+
...unusedClasses.slice(0, Math.min(unusedReportLimit, unusedClasses.length)),
|
|
932
|
+
...unusedStaticClasses.slice(0, Math.min(unusedReportLimit, unusedStaticClasses.length)),
|
|
933
|
+
].slice(0, displayLimit);
|
|
934
|
+
|
|
935
|
+
if (displayClasses.length > 0) {
|
|
936
|
+
console.log(` 示例 (前 ${displayLimit} 个):`);
|
|
937
|
+
displayClasses.forEach((cls, index) => {
|
|
938
|
+
if (index < 20) {
|
|
939
|
+
// 只显示前 20 个,避免输出过长
|
|
940
|
+
console.log(` - ${cls}`);
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
if (displayClasses.length > 20) {
|
|
944
|
+
console.log(` ... 还有 ${displayClasses.length - 20} 个未显示`);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if (totalUnused > displayLimit) {
|
|
949
|
+
console.log(` (仅显示前 ${displayLimit} 个,实际有 ${totalUnused} 个未使用的 class)`);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
console.log(' 提示: 这些 class 在输出文件中存在,但当前项目扫描未使用。');
|
|
953
|
+
console.log(' 建议: 可以手动清理输出文件中的这些 class,或保留它们以备将来使用。\n');
|
|
954
|
+
}
|
|
955
|
+
} catch (error) {
|
|
956
|
+
// 报告失败不影响正常流程
|
|
957
|
+
this.logger.warn(`检查未使用 class 失败: ${error.message}`);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// 计算统一文件模式的构建时间(取最早开始时间)
|
|
962
|
+
calculateBuildTimeForUnified(filePaths) {
|
|
963
|
+
if (!filePaths || filePaths.length === 0) {
|
|
964
|
+
return null;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// 找到最早的开始时间
|
|
968
|
+
let earliestStartTime = null;
|
|
969
|
+
for (const filePath of filePaths) {
|
|
970
|
+
const startTime = this.buildStartTimes.get(filePath);
|
|
971
|
+
if (startTime && (earliestStartTime === null || startTime < earliestStartTime)) {
|
|
972
|
+
earliestStartTime = startTime;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
if (earliestStartTime) {
|
|
977
|
+
const buildTime = Date.now() - earliestStartTime;
|
|
978
|
+
// 清除所有相关文件的构建时间记录
|
|
979
|
+
for (const filePath of filePaths) {
|
|
980
|
+
this.buildStartTimes.delete(filePath);
|
|
981
|
+
}
|
|
982
|
+
return buildTime;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
return null;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// 获取状态信息
|
|
989
|
+
getStatus() {
|
|
990
|
+
return {
|
|
991
|
+
isInitialized: this.isInitialized,
|
|
992
|
+
isRunning: this.isRunning,
|
|
993
|
+
state: this.stateManager.getStats(),
|
|
994
|
+
config: this.configManager.getConfigInfo(),
|
|
995
|
+
cache: this.cacheManager.getCacheStats(),
|
|
996
|
+
throttle: this.throttle.getStats(),
|
|
997
|
+
parser: this.classParser.getParseStats(),
|
|
998
|
+
generator: this.dynamicClassGenerator.getGenerationStats(),
|
|
999
|
+
watcher: this.fileWatcher.getWatchStats(),
|
|
1000
|
+
configWatcher: this.configWatcher.getWatcherStats(),
|
|
1001
|
+
writer: this.fileWriter.getWriteStats(),
|
|
1002
|
+
fullScan: this.fullScanManager.getStats(),
|
|
1003
|
+
unifiedWriter: this.unifiedWriter.getWriteStats(),
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// 获取事件总线(用于外部监听)
|
|
1008
|
+
getEventBus() {
|
|
1009
|
+
return this.eventBus;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// 获取各个模块(用于高级用法)
|
|
1013
|
+
getModules() {
|
|
1014
|
+
return {
|
|
1015
|
+
stateManager: this.stateManager,
|
|
1016
|
+
configManager: this.configManager,
|
|
1017
|
+
cacheManager: this.cacheManager,
|
|
1018
|
+
logger: this.logger,
|
|
1019
|
+
throttle: this.throttle,
|
|
1020
|
+
fileUtils: this.fileUtils,
|
|
1021
|
+
regexCompiler: this.regexCompiler,
|
|
1022
|
+
importantParser: this.importantParser,
|
|
1023
|
+
classParser: this.classParser,
|
|
1024
|
+
dynamicClassGenerator: this.dynamicClassGenerator,
|
|
1025
|
+
fileWatcher: this.fileWatcher,
|
|
1026
|
+
configWatcher: this.configWatcher,
|
|
1027
|
+
fileWriter: this.fileWriter,
|
|
1028
|
+
fullScanManager: this.fullScanManager,
|
|
1029
|
+
unifiedWriter: this.unifiedWriter,
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// 重置所有状态
|
|
1034
|
+
reset() {
|
|
1035
|
+
try {
|
|
1036
|
+
this.stop();
|
|
1037
|
+
this.stateManager.reset();
|
|
1038
|
+
this.cacheManager.clearAll();
|
|
1039
|
+
this.logger.info('Class2CSS 重置完成');
|
|
1040
|
+
} catch (error) {
|
|
1041
|
+
this.logger.error('重置 Class2CSS 时出错:', error);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
module.exports = Class2CSS;
|