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
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
class CssFormatter {
|
|
2
|
+
constructor(format = 'multiLine') {
|
|
3
|
+
this.format = format;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// 转义 CSS 选择器中的特殊字符
|
|
7
|
+
// 确保生成的 CSS 选择器能正确匹配 HTML/WXML 中的 class 名
|
|
8
|
+
escapeSelector(selector) {
|
|
9
|
+
if (!selector || typeof selector !== 'string') {
|
|
10
|
+
return selector;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// 转义特殊字符:冒号、感叹号、空格、斜杠等
|
|
14
|
+
// CSS 选择器中需要转义的字符:: ! / \ 空格等
|
|
15
|
+
// 使用反斜杠转义,例如 sm:w-100 -> sm\:w-100
|
|
16
|
+
return selector.replace(/([:!\/\\\s])/g, '\\$1');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 设置格式
|
|
20
|
+
setFormat(format) {
|
|
21
|
+
if (['multiLine', 'singleLine', 'compressed'].includes(format)) {
|
|
22
|
+
this.format = format;
|
|
23
|
+
} else {
|
|
24
|
+
this.format = 'multiLine'; // 默认格式
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 获取当前格式
|
|
29
|
+
getFormat() {
|
|
30
|
+
return this.format;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 格式化单个CSS规则
|
|
34
|
+
formatRule(selector, properties, format = null) {
|
|
35
|
+
const targetFormat = format || this.format;
|
|
36
|
+
|
|
37
|
+
if (targetFormat === 'compressed') {
|
|
38
|
+
return this.formatRuleCompressed(selector, properties);
|
|
39
|
+
} else if (targetFormat === 'singleLine') {
|
|
40
|
+
return this.formatRuleSingleLine(selector, properties);
|
|
41
|
+
} else {
|
|
42
|
+
return this.formatRuleMultiLine(selector, properties);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 多行格式(默认)
|
|
47
|
+
formatRuleMultiLine(selector, properties) {
|
|
48
|
+
// 转义选择器中的特殊字符
|
|
49
|
+
const escapedSelector = this.escapeSelector(selector);
|
|
50
|
+
|
|
51
|
+
if (Array.isArray(properties)) {
|
|
52
|
+
// 属性数组格式:[{property: 'margin', value: '10rpx'}, ...]
|
|
53
|
+
let css = `\n.${escapedSelector} {\n`;
|
|
54
|
+
properties.forEach(({ property, value }) => {
|
|
55
|
+
css += ` ${property}: ${value};\n`;
|
|
56
|
+
});
|
|
57
|
+
css += `}\n`;
|
|
58
|
+
return css;
|
|
59
|
+
} else if (typeof properties === 'string') {
|
|
60
|
+
// 字符串格式:'margin: 10rpx;'
|
|
61
|
+
return `\n.${escapedSelector} {\n ${properties}\n}\n`;
|
|
62
|
+
}
|
|
63
|
+
return '';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 单行格式
|
|
67
|
+
formatRuleSingleLine(selector, properties) {
|
|
68
|
+
// 转义选择器中的特殊字符
|
|
69
|
+
const escapedSelector = this.escapeSelector(selector);
|
|
70
|
+
|
|
71
|
+
if (Array.isArray(properties)) {
|
|
72
|
+
// 属性数组格式
|
|
73
|
+
const propsStr = properties.map(({ property, value }) => `${property}: ${value}`).join('; ');
|
|
74
|
+
return `.${escapedSelector} { ${propsStr}; }\n`;
|
|
75
|
+
} else if (typeof properties === 'string') {
|
|
76
|
+
// 字符串格式
|
|
77
|
+
return `.${escapedSelector} { ${properties}; }\n`;
|
|
78
|
+
}
|
|
79
|
+
return '';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 压缩格式
|
|
83
|
+
formatRuleCompressed(selector, properties) {
|
|
84
|
+
// 转义选择器中的特殊字符
|
|
85
|
+
const escapedSelector = this.escapeSelector(selector);
|
|
86
|
+
|
|
87
|
+
if (Array.isArray(properties)) {
|
|
88
|
+
// 属性数组格式
|
|
89
|
+
const propsStr = properties.map(({ property, value }) => `${property}:${value}`).join(';');
|
|
90
|
+
return `.${escapedSelector}{${propsStr}}`;
|
|
91
|
+
} else if (typeof properties === 'string') {
|
|
92
|
+
// 字符串格式,移除空格但保留分号(除了最后一个分号)
|
|
93
|
+
let cleanProps = properties.replace(/\s+/g, ''); // 移除所有空格
|
|
94
|
+
// 确保属性之间有分号分隔,但移除末尾的分号
|
|
95
|
+
cleanProps = cleanProps.replace(/;+$/, ''); // 移除末尾的分号
|
|
96
|
+
// 确保分号后面没有空格(虽然已经移除了空格,但为了安全)
|
|
97
|
+
cleanProps = cleanProps.replace(/;+/g, ';'); // 多个分号合并为一个
|
|
98
|
+
return `.${escapedSelector}{${cleanProps}}`;
|
|
99
|
+
}
|
|
100
|
+
return '';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 格式化完整CSS字符串
|
|
104
|
+
formatCSS(cssString, format = null) {
|
|
105
|
+
if (!cssString || typeof cssString !== 'string') {
|
|
106
|
+
return '';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const targetFormat = format || this.format;
|
|
110
|
+
|
|
111
|
+
if (targetFormat === 'compressed') {
|
|
112
|
+
return this.compressCSS(cssString);
|
|
113
|
+
} else if (targetFormat === 'singleLine') {
|
|
114
|
+
return this.singleLineCSS(cssString);
|
|
115
|
+
} else {
|
|
116
|
+
return this.multiLineCSS(cssString);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 压缩CSS(去除所有空格、换行、注释)
|
|
121
|
+
compressCSS(cssString) {
|
|
122
|
+
return cssString
|
|
123
|
+
.replace(/\/\*[\s\S]*?\*\//g, '') // 移除注释
|
|
124
|
+
.replace(/\s+/g, ' ') // 多个空格合并为一个
|
|
125
|
+
.replace(/\s*{\s*/g, '{') // 移除大括号周围的空格
|
|
126
|
+
.replace(/\s*}\s*/g, '}') // 移除大括号周围的空格
|
|
127
|
+
.replace(/\s*:\s*/g, ':') // 移除冒号周围的空格
|
|
128
|
+
.replace(/\s*;\s*/g, ';') // 移除分号周围的空格(保留分号)
|
|
129
|
+
.replace(/\s*,\s*/g, ',') // 移除逗号周围的空格
|
|
130
|
+
.replace(/;\s*}/g, '}') // 移除结束大括号前的分号(CSS规则结束不需要分号)
|
|
131
|
+
.replace(/\s+/g, '') // 移除所有剩余空格
|
|
132
|
+
.trim();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 单行CSS(每个规则单行,但规则之间换行)
|
|
136
|
+
singleLineCSS(cssString) {
|
|
137
|
+
// 先压缩,然后按规则分割,每行一个规则
|
|
138
|
+
const compressed = this.compressCSS(cssString);
|
|
139
|
+
// 按 } 分割规则
|
|
140
|
+
const rules = compressed.split('}').filter(rule => rule.trim());
|
|
141
|
+
return rules.map(rule => rule.trim() + '}').join('\n') + '\n';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 多行CSS(格式化,保持可读性)
|
|
145
|
+
multiLineCSS(cssString) {
|
|
146
|
+
// 如果已经是多行格式,直接返回
|
|
147
|
+
if (cssString.includes('\n')) {
|
|
148
|
+
return cssString;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 将压缩的CSS转换为多行格式
|
|
152
|
+
let formatted = cssString
|
|
153
|
+
.replace(/\{/g, ' {\n ')
|
|
154
|
+
.replace(/;/g, ';\n ')
|
|
155
|
+
.replace(/\}/g, '\n}\n')
|
|
156
|
+
.replace(/\n\s*\n/g, '\n'); // 移除多余空行
|
|
157
|
+
|
|
158
|
+
// 清理格式
|
|
159
|
+
formatted = formatted
|
|
160
|
+
.split('\n')
|
|
161
|
+
.map(line => line.trim())
|
|
162
|
+
.filter(line => line.length > 0)
|
|
163
|
+
.map((line, index, array) => {
|
|
164
|
+
if (line.endsWith('{')) {
|
|
165
|
+
return line;
|
|
166
|
+
} else if (line === '}') {
|
|
167
|
+
return line;
|
|
168
|
+
} else if (line.endsWith(';')) {
|
|
169
|
+
return ' ' + line;
|
|
170
|
+
}
|
|
171
|
+
return ' ' + line;
|
|
172
|
+
})
|
|
173
|
+
.join('\n') + '\n';
|
|
174
|
+
|
|
175
|
+
return formatted;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 格式化属性值对数组为字符串
|
|
179
|
+
formatProperties(properties, format = null) {
|
|
180
|
+
const targetFormat = format || this.format;
|
|
181
|
+
|
|
182
|
+
if (Array.isArray(properties)) {
|
|
183
|
+
if (targetFormat === 'compressed') {
|
|
184
|
+
return properties.map(({ property, value }) => `${property}:${value}`).join(';');
|
|
185
|
+
} else if (targetFormat === 'singleLine') {
|
|
186
|
+
return properties.map(({ property, value }) => `${property}: ${value}`).join('; ');
|
|
187
|
+
} else {
|
|
188
|
+
return properties.map(({ property, value }) => ` ${property}: ${value};\n`).join('');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return '';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 对CSS规则进行字母排序
|
|
195
|
+
sortCSSRules(cssString) {
|
|
196
|
+
if (!cssString || typeof cssString !== 'string') {
|
|
197
|
+
return '';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 解析CSS规则:逐字符解析,跟踪大括号嵌套
|
|
201
|
+
const rules = [];
|
|
202
|
+
let currentRule = '';
|
|
203
|
+
let braceCount = 0;
|
|
204
|
+
let selector = '';
|
|
205
|
+
let i = 0;
|
|
206
|
+
|
|
207
|
+
while (i < cssString.length) {
|
|
208
|
+
const char = cssString[i];
|
|
209
|
+
|
|
210
|
+
if (char === '{') {
|
|
211
|
+
if (braceCount === 0) {
|
|
212
|
+
// 找到选择器结束
|
|
213
|
+
selector = currentRule.trim();
|
|
214
|
+
currentRule = '{';
|
|
215
|
+
} else {
|
|
216
|
+
currentRule += char;
|
|
217
|
+
}
|
|
218
|
+
braceCount++;
|
|
219
|
+
} else if (char === '}') {
|
|
220
|
+
currentRule += char;
|
|
221
|
+
braceCount--;
|
|
222
|
+
if (braceCount === 0) {
|
|
223
|
+
// 找到规则结束
|
|
224
|
+
// 清理选择器:移除点号前缀用于排序(注意:转义字符不需要处理,排序时保持原样)
|
|
225
|
+
const cleanSelector = selector.replace(/^\./, '').trim();
|
|
226
|
+
if (cleanSelector) {
|
|
227
|
+
rules.push({
|
|
228
|
+
selector: cleanSelector,
|
|
229
|
+
fullRule: selector + currentRule,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
currentRule = '';
|
|
233
|
+
selector = '';
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
currentRule += char;
|
|
237
|
+
}
|
|
238
|
+
i++;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 如果没有匹配到规则,返回原字符串
|
|
242
|
+
if (rules.length === 0) {
|
|
243
|
+
return cssString;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 按选择器名称进行字母排序(不区分大小写)
|
|
247
|
+
rules.sort((a, b) => {
|
|
248
|
+
const selectorA = a.selector.toLowerCase();
|
|
249
|
+
const selectorB = b.selector.toLowerCase();
|
|
250
|
+
if (selectorA < selectorB) return -1;
|
|
251
|
+
if (selectorA > selectorB) return 1;
|
|
252
|
+
return 0;
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// 重新组合CSS规则
|
|
256
|
+
return rules.map(rule => rule.fullRule).join('');
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
module.exports = CssFormatter;
|
|
261
|
+
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs').promises;
|
|
3
|
+
|
|
4
|
+
class FileUtils {
|
|
5
|
+
constructor(eventBus) {
|
|
6
|
+
this.eventBus = eventBus;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// 文件信息提取
|
|
10
|
+
getFileInfo(filePath) {
|
|
11
|
+
try {
|
|
12
|
+
const fileName = path.basename(filePath);
|
|
13
|
+
const fileType = path.extname(filePath).substring(1);
|
|
14
|
+
const fileDir = path.dirname(filePath);
|
|
15
|
+
const fileTName = path.basename(filePath, path.extname(filePath));
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
fileName,
|
|
19
|
+
fileType,
|
|
20
|
+
filePath: fileDir,
|
|
21
|
+
fileTName,
|
|
22
|
+
path: filePath,
|
|
23
|
+
fullPath: path.resolve(filePath),
|
|
24
|
+
};
|
|
25
|
+
} catch (error) {
|
|
26
|
+
this.eventBus.emit('fileUtils:error', { operation: 'getFileInfo', filePath, error });
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 路径标准化
|
|
32
|
+
normalizePath(filePath) {
|
|
33
|
+
return filePath.replace(/\\/g, '/');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 路径合并
|
|
37
|
+
joinPaths(...paths) {
|
|
38
|
+
return path.join(...paths).replace(/\\/g, '/');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 文件存在检查
|
|
42
|
+
async fileExists(filePath) {
|
|
43
|
+
try {
|
|
44
|
+
await fs.access(filePath);
|
|
45
|
+
return true;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 目录存在检查
|
|
52
|
+
async directoryExists(dirPath) {
|
|
53
|
+
try {
|
|
54
|
+
const stat = await fs.stat(dirPath);
|
|
55
|
+
return stat.isDirectory();
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 创建目录
|
|
62
|
+
async createDirectory(dirPath) {
|
|
63
|
+
try {
|
|
64
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
65
|
+
this.eventBus.emit('fileUtils:directory:created', dirPath);
|
|
66
|
+
return true;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
this.eventBus.emit('fileUtils:error', { operation: 'createDirectory', dirPath, error });
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 读取文件
|
|
74
|
+
async readFile(filePath, encoding = 'utf-8') {
|
|
75
|
+
try {
|
|
76
|
+
const content = await fs.readFile(filePath, encoding);
|
|
77
|
+
this.eventBus.emit('fileUtils:file:read', filePath);
|
|
78
|
+
return content;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
this.eventBus.emit('fileUtils:error', { operation: 'readFile', filePath, error });
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 写入文件
|
|
86
|
+
async writeFile(filePath, content, encoding = 'utf-8') {
|
|
87
|
+
try {
|
|
88
|
+
// 确保目录存在
|
|
89
|
+
const dirPath = path.dirname(filePath);
|
|
90
|
+
await this.createDirectory(dirPath);
|
|
91
|
+
|
|
92
|
+
await fs.writeFile(filePath, content, encoding);
|
|
93
|
+
this.eventBus.emit('fileUtils:file:written', filePath);
|
|
94
|
+
return true;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
this.eventBus.emit('fileUtils:error', { operation: 'writeFile', filePath, error });
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 获取文件统计信息
|
|
102
|
+
async getFileStats(filePath) {
|
|
103
|
+
try {
|
|
104
|
+
const stats = await fs.stat(filePath);
|
|
105
|
+
return {
|
|
106
|
+
size: stats.size,
|
|
107
|
+
mtime: stats.mtime,
|
|
108
|
+
ctime: stats.ctime,
|
|
109
|
+
isFile: stats.isFile(),
|
|
110
|
+
isDirectory: stats.isDirectory(),
|
|
111
|
+
};
|
|
112
|
+
} catch (error) {
|
|
113
|
+
this.eventBus.emit('fileUtils:error', { operation: 'getFileStats', filePath, error });
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 文件类型检查
|
|
119
|
+
isHtmlFile(filePath) {
|
|
120
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
121
|
+
return ['.html', '.htm'].includes(ext);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
isWxmlFile(filePath) {
|
|
125
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
126
|
+
return ext === '.wxml';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
isSupportedFile(filePath, supportedTypes = ['html', 'wxml']) {
|
|
130
|
+
const ext = path.extname(filePath).toLowerCase().substring(1);
|
|
131
|
+
return supportedTypes.includes(ext);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 路径模式匹配
|
|
135
|
+
matchPathPattern(filePath, patterns) {
|
|
136
|
+
const normalizedPath = this.normalizePath(filePath);
|
|
137
|
+
|
|
138
|
+
return patterns.some((pattern) => {
|
|
139
|
+
// 简单的通配符匹配
|
|
140
|
+
const regexPattern = pattern.replace(/\\/g, '/').replace(/\*/g, '.*').replace(/\?/g, '.');
|
|
141
|
+
|
|
142
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
143
|
+
return regex.test(normalizedPath);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 相对路径计算
|
|
148
|
+
getRelativePath(fromPath, toPath) {
|
|
149
|
+
try {
|
|
150
|
+
return path.relative(fromPath, toPath).replace(/\\/g, '/');
|
|
151
|
+
} catch (error) {
|
|
152
|
+
this.eventBus.emit('fileUtils:error', {
|
|
153
|
+
operation: 'getRelativePath',
|
|
154
|
+
fromPath,
|
|
155
|
+
toPath,
|
|
156
|
+
error,
|
|
157
|
+
});
|
|
158
|
+
return toPath;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 绝对路径转换
|
|
163
|
+
getAbsolutePath(filePath, basePath = process.cwd()) {
|
|
164
|
+
try {
|
|
165
|
+
return path.resolve(basePath, filePath).replace(/\\/g, '/');
|
|
166
|
+
} catch (error) {
|
|
167
|
+
this.eventBus.emit('fileUtils:error', {
|
|
168
|
+
operation: 'getAbsolutePath',
|
|
169
|
+
filePath,
|
|
170
|
+
basePath,
|
|
171
|
+
error,
|
|
172
|
+
});
|
|
173
|
+
return filePath;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 文件扩展名处理
|
|
178
|
+
changeExtension(filePath, newExt) {
|
|
179
|
+
const ext = path.extname(filePath);
|
|
180
|
+
const basePath = filePath.substring(0, filePath.length - ext.length);
|
|
181
|
+
return `${basePath}${newExt.startsWith('.') ? newExt : `.${newExt}`}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 批量文件操作
|
|
185
|
+
async batchProcess(files, processor, options = {}) {
|
|
186
|
+
const results = [];
|
|
187
|
+
const { concurrency = 5, onProgress } = options;
|
|
188
|
+
|
|
189
|
+
for (let i = 0; i < files.length; i += concurrency) {
|
|
190
|
+
const batch = files.slice(i, i + concurrency);
|
|
191
|
+
const batchPromises = batch.map(async (file) => {
|
|
192
|
+
try {
|
|
193
|
+
const result = await processor(file);
|
|
194
|
+
return { file, success: true, result };
|
|
195
|
+
} catch (error) {
|
|
196
|
+
return { file, success: false, error };
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const batchResults = await Promise.all(batchPromises);
|
|
201
|
+
results.push(...batchResults);
|
|
202
|
+
|
|
203
|
+
if (onProgress) {
|
|
204
|
+
onProgress(i + batch.length, files.length);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
this.eventBus.emit('fileUtils:batch:completed', { totalFiles: files.length, results });
|
|
209
|
+
return results;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 文件监控辅助
|
|
213
|
+
getFileChangeType(oldStats, newStats) {
|
|
214
|
+
if (!oldStats || !newStats) {
|
|
215
|
+
return 'unknown';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (oldStats.mtime.getTime() !== newStats.mtime.getTime()) {
|
|
219
|
+
return 'modified';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (oldStats.size !== newStats.size) {
|
|
223
|
+
return 'size_changed';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return 'unchanged';
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
module.exports = FileUtils;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
class Logger {
|
|
2
|
+
constructor(eventBus, options = {}) {
|
|
3
|
+
this.eventBus = eventBus;
|
|
4
|
+
this.level = options.level || 'info'; // debug, info, warn, error
|
|
5
|
+
this.enableTimestamp = options.enableTimestamp !== false;
|
|
6
|
+
this.enableDebug = options.enableDebug || false;
|
|
7
|
+
|
|
8
|
+
this.levels = {
|
|
9
|
+
debug: 0,
|
|
10
|
+
info: 1,
|
|
11
|
+
warn: 2,
|
|
12
|
+
error: 3,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
formatMessage(level, message) {
|
|
17
|
+
const timestamp = this.enableTimestamp ? `[${new Date().toLocaleString()}]` : '';
|
|
18
|
+
const levelTag = `[${level.toUpperCase()}]`;
|
|
19
|
+
return `${timestamp} ${levelTag} ${message}`.trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
shouldLog(level) {
|
|
23
|
+
return this.levels[level] >= this.levels[this.level];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
debug(message, ...args) {
|
|
27
|
+
if (this.shouldLog('debug') && this.enableDebug) {
|
|
28
|
+
const formattedMessage = this.formatMessage('debug', message);
|
|
29
|
+
console.log(formattedMessage, ...args);
|
|
30
|
+
this.eventBus.emit('log:debug', { message, args });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
info(message, ...args) {
|
|
35
|
+
if (this.shouldLog('info')) {
|
|
36
|
+
const formattedMessage = this.formatMessage('info', message);
|
|
37
|
+
console.log(formattedMessage, ...args);
|
|
38
|
+
this.eventBus.emit('log:info', { message, args });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
warn(message, ...args) {
|
|
43
|
+
if (this.shouldLog('warn')) {
|
|
44
|
+
const formattedMessage = this.formatMessage('warn', message);
|
|
45
|
+
console.warn(formattedMessage, ...args);
|
|
46
|
+
this.eventBus.emit('log:warn', { message, args });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
error(message, ...args) {
|
|
51
|
+
if (this.shouldLog('error')) {
|
|
52
|
+
const formattedMessage = this.formatMessage('error', message);
|
|
53
|
+
console.error(formattedMessage, ...args);
|
|
54
|
+
this.eventBus.emit('log:error', { message, args });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 特定领域的日志方法
|
|
59
|
+
config(message, ...args) {
|
|
60
|
+
this.info(`[CONFIG] ${message}`, ...args);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
cache(message, ...args) {
|
|
64
|
+
this.debug(`[CACHE] ${message}`, ...args);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
parser(message, ...args) {
|
|
68
|
+
this.debug(`[PARSER] ${message}`, ...args);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
generator(message, ...args) {
|
|
72
|
+
this.debug(`[GENERATOR] ${message}`, ...args);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
watcher(message, ...args) {
|
|
76
|
+
this.info(`[WATCHER] ${message}`, ...args);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
writer(message, ...args) {
|
|
80
|
+
this.info(`[WRITER] ${message}`, ...args);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
scan(message, ...args) {
|
|
84
|
+
this.info(`[SCAN] ${message}`, ...args);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 日志级别设置
|
|
88
|
+
setLevel(level) {
|
|
89
|
+
if (this.levels.hasOwnProperty(level)) {
|
|
90
|
+
this.level = level;
|
|
91
|
+
this.info(`Log level changed to: ${level}`);
|
|
92
|
+
this.eventBus.emit('log:level:changed', level);
|
|
93
|
+
} else {
|
|
94
|
+
this.error(`Invalid log level: ${level}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
getLevel() {
|
|
99
|
+
return this.level;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 调试模式切换
|
|
103
|
+
setDebugMode(enabled) {
|
|
104
|
+
this.enableDebug = enabled;
|
|
105
|
+
this.info(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
|
|
106
|
+
this.eventBus.emit('log:debug:changed', enabled);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 时间戳切换
|
|
110
|
+
setTimestamp(enabled) {
|
|
111
|
+
this.enableTimestamp = enabled;
|
|
112
|
+
this.info(`Timestamp ${enabled ? 'enabled' : 'disabled'}`);
|
|
113
|
+
this.eventBus.emit('log:timestamp:changed', enabled);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 性能日志
|
|
117
|
+
time(label) {
|
|
118
|
+
if (this.shouldLog('debug')) {
|
|
119
|
+
console.time(`[PERF] ${label}`);
|
|
120
|
+
this.eventBus.emit('log:time:start', label);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
timeEnd(label) {
|
|
125
|
+
if (this.shouldLog('debug')) {
|
|
126
|
+
console.timeEnd(`[PERF] ${label}`);
|
|
127
|
+
this.eventBus.emit('log:time:end', label);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 统计信息日志
|
|
132
|
+
stats(stats) {
|
|
133
|
+
this.info('[STATS]', stats);
|
|
134
|
+
this.eventBus.emit('log:stats', stats);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 错误日志增强
|
|
138
|
+
errorWithContext(message, error, context = {}) {
|
|
139
|
+
this.error(message);
|
|
140
|
+
if (error && error.stack) {
|
|
141
|
+
this.error('Stack trace:', error.stack);
|
|
142
|
+
}
|
|
143
|
+
if (Object.keys(context).length > 0) {
|
|
144
|
+
this.error('Context:', context);
|
|
145
|
+
}
|
|
146
|
+
this.eventBus.emit('log:error:withContext', { message, error, context });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = Logger;
|