mm_scanner 0.0.0-stage → 1.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/example/data/app/app.json +5 -0
- package/example/data/config/db.json +4 -0
- package/example/data/config/logger.js +3 -0
- package/example/data/dist/build.json +3 -0
- package/example/data/readme.txt +1 -0
- package/example/demo.js +62 -0
- package/index.js +16 -0
- package/lib/scanner.js +264 -0
- package/package.json +41 -4
- package/README.md +0 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
演示文件,不会被默认配置扫描
|
package/example/demo.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { Scanner, scannerAdmin } = require('../index');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 使用示例:扫描 example/data 目录并按多种方式检索文件
|
|
8
|
+
*/
|
|
9
|
+
function demo() {
|
|
10
|
+
const dir = path.join(__dirname, 'data');
|
|
11
|
+
|
|
12
|
+
// 1. 创建扫描器,默认只扫描 .json,并忽略 node_modules 与 .git
|
|
13
|
+
const scanner = new Scanner();
|
|
14
|
+
console.log('=== 默认配置 ===');
|
|
15
|
+
console.log(' 扩展名: ' + scanner.extensions.join(', '));
|
|
16
|
+
console.log(' 忽略目录: ' + scanner.config.ignore.join(', '));
|
|
17
|
+
|
|
18
|
+
// 2. 扫描目录,可重复调用(仅首次生效)
|
|
19
|
+
scanner.scan(dir);
|
|
20
|
+
console.log('=== 扫描结果 ===');
|
|
21
|
+
console.log(' 基目录: ' + scanner.baseDir);
|
|
22
|
+
console.log(' 文件数: ' + scanner.size);
|
|
23
|
+
for (const key of scanner.keys()) {
|
|
24
|
+
console.log(' ' + key);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 3. 按 key 查询,key 可省略扩展名
|
|
28
|
+
console.log('=== 按 key 查询 ===');
|
|
29
|
+
console.log(' 元信息: ' + JSON.stringify(scanner.get('app/app')));
|
|
30
|
+
console.log(' 绝对路径: ' + scanner.getPath('config/db'));
|
|
31
|
+
console.log(' 是否存在 config/logger: ' + scanner.has('config/logger'));
|
|
32
|
+
|
|
33
|
+
// 4. 按扩展名、目录、层级筛选
|
|
34
|
+
console.log('=== 分类检索 ===');
|
|
35
|
+
console.log(' .json 文件: ' + scanner.getByExt('.json').length + ' 个');
|
|
36
|
+
console.log(' config 目录: ' + scanner.getByDir('config').map((f) => f.key).join(', '));
|
|
37
|
+
console.log(' 一级子目录: ' + scanner.dirTypes().join(', '));
|
|
38
|
+
console.log(' 扩展名类型: ' + scanner.extTypes().join(', '));
|
|
39
|
+
|
|
40
|
+
// 5. 自定义扩展名与忽略目录
|
|
41
|
+
const custom = new Scanner({ extensions: ['json', 'js'], ignore: ['dist'] });
|
|
42
|
+
custom.scan(dir);
|
|
43
|
+
console.log('=== 自定义配置 ===');
|
|
44
|
+
console.log(' 文件数: ' + custom.size);
|
|
45
|
+
console.log(' 文件列表: ' + custom.keys().join(', '));
|
|
46
|
+
console.log(' dist 目录已被忽略: ' + (custom.getByDir('dist').length === 0));
|
|
47
|
+
|
|
48
|
+
// 6. 文件变化后重新扫描
|
|
49
|
+
custom.rescan();
|
|
50
|
+
console.log('=== 重新扫描 ===');
|
|
51
|
+
console.log(' 文件数: ' + custom.size);
|
|
52
|
+
|
|
53
|
+
// 7. 通过 admin 按作用域复用扫描器实例
|
|
54
|
+
const os_scanner = scannerAdmin('os', { extensions: ['json'] });
|
|
55
|
+
const demo_scanner = scannerAdmin('demo');
|
|
56
|
+
console.log('=== 作用域实例 ===');
|
|
57
|
+
console.log(' os 实例类型正确: ' + (os_scanner instanceof Scanner));
|
|
58
|
+
console.log(' 重复获取 os 实例复用缓存: ' + (scannerAdmin('os') === os_scanner));
|
|
59
|
+
console.log(' demo 实例与 os 实例相互独立: ' + (demo_scanner !== os_scanner));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
demo();
|
package/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const Scanner = require('./lib/scanner');
|
|
2
|
+
const Admin = require('mm_corejs').Admin;
|
|
3
|
+
|
|
4
|
+
const admin = new Admin();
|
|
5
|
+
admin.setClass(Scanner);
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
Scanner,
|
|
9
|
+
admin,
|
|
10
|
+
scannerAdmin(scope, config) {
|
|
11
|
+
if (!scope) {
|
|
12
|
+
scope = 'os';
|
|
13
|
+
}
|
|
14
|
+
return admin.get(scope, config);
|
|
15
|
+
}
|
|
16
|
+
};
|
package/lib/scanner.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/* key 规范化:只统一分隔符、去首尾 /,不再剥扩展名 */
|
|
7
|
+
function normalizeKey(p) {
|
|
8
|
+
return String(p)
|
|
9
|
+
.replace(/\\/g, '/')
|
|
10
|
+
.replace(/^\.\//, '')
|
|
11
|
+
.replace(/^\/+|\/+$/g, '');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* 扩展名规范化:补点、转小写 */
|
|
15
|
+
function normalizeExt(ext) {
|
|
16
|
+
const e = String(ext).trim().toLowerCase();
|
|
17
|
+
return e.startsWith('.') ? e : '.' + e;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 合并默认配置与用户配置
|
|
22
|
+
* @param {{ extensions: string[], ignore: string[] }} default_config - 默认配置
|
|
23
|
+
* @param {{ extensions?: string[], ignore?: string[] }} config - 用户配置
|
|
24
|
+
* @returns {{ extensions: string[], ignore: string[] }} 规范化后的配置
|
|
25
|
+
*/
|
|
26
|
+
function createConfig(default_config, config) {
|
|
27
|
+
const extensions =
|
|
28
|
+
Array.isArray(config.extensions) && config.extensions.length
|
|
29
|
+
? config.extensions
|
|
30
|
+
: default_config.extensions;
|
|
31
|
+
const ignore = Array.isArray(config.ignore) ? config.ignore : default_config.ignore;
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
extensions: extensions.map(normalizeExt),
|
|
35
|
+
ignore: [...ignore],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
class Scanner {
|
|
40
|
+
/**
|
|
41
|
+
* 扫描器默认配置
|
|
42
|
+
*/
|
|
43
|
+
static config = {
|
|
44
|
+
/** 支持的扩展名 */
|
|
45
|
+
extensions: ['.json'],
|
|
46
|
+
/** 忽略的目录 */
|
|
47
|
+
ignore: ['node_modules', '.git'],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** key(含扩展名) -> { key, absPath, relPath, dir, name, ext } */
|
|
51
|
+
#files = new Map();
|
|
52
|
+
#baseDir = null;
|
|
53
|
+
#scanned = false;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 创建扫描器实例
|
|
57
|
+
* @param {{ extensions?: string[], ignore?: string[] }} [config] - 扫描器配置
|
|
58
|
+
*/
|
|
59
|
+
constructor(config = {}) {
|
|
60
|
+
this.config = createConfig(Scanner.config, config);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* -------- 私有 -------- */
|
|
64
|
+
|
|
65
|
+
#walk(dir) {
|
|
66
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
67
|
+
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
const fullPath = path.join(dir, entry.name);
|
|
70
|
+
|
|
71
|
+
if (entry.isDirectory()) {
|
|
72
|
+
if (this.config.ignore.includes(entry.name)) continue;
|
|
73
|
+
this.#walk(fullPath);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!entry.isFile()) continue;
|
|
78
|
+
|
|
79
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
80
|
+
if (!this.config.extensions.includes(ext)) continue;
|
|
81
|
+
|
|
82
|
+
const relPath = path.relative(this.#baseDir, fullPath).replace(/\\/g, '/');
|
|
83
|
+
const key = normalizeKey(relPath); // 保留扩展名,如 'config/app.json'
|
|
84
|
+
const dirPart = path.posix.dirname(relPath);
|
|
85
|
+
|
|
86
|
+
this.#files.set(key, {
|
|
87
|
+
key,
|
|
88
|
+
absPath: fullPath,
|
|
89
|
+
relPath,
|
|
90
|
+
dir: dirPart === '.' ? '' : dirPart,
|
|
91
|
+
name: path.basename(relPath, ext),
|
|
92
|
+
ext,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#ensureReady() {
|
|
98
|
+
if (!this.#scanned) {
|
|
99
|
+
throw new Error('[scanner] 尚未初始化,请先在入口调用 scanner.scan(dir)');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 宽松解析 key:精确命中 → 按扩展名顺序尝试补全 */
|
|
104
|
+
#resolveKey(input) {
|
|
105
|
+
const norm = normalizeKey(input);
|
|
106
|
+
if (this.#files.has(norm)) return norm;
|
|
107
|
+
|
|
108
|
+
for (const ext of this.config.extensions) {
|
|
109
|
+
const tryKey = norm + ext;
|
|
110
|
+
if (this.#files.has(tryKey)) return tryKey;
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 判断文件的相对目录是否命中目标目录
|
|
117
|
+
* @param {string} dir_path - 文件所在相对目录
|
|
118
|
+
* @param {string} dir_key - 规范化后的目标目录
|
|
119
|
+
* @returns {boolean} 命中返回 true,目标目录为空时视为全部命中
|
|
120
|
+
*/
|
|
121
|
+
#matchDir(dir_path, dir_key) {
|
|
122
|
+
if (!dir_key) return true;
|
|
123
|
+
if (dir_key.includes('/')) {
|
|
124
|
+
return dir_path === dir_key || dir_path.endsWith('/' + dir_key);
|
|
125
|
+
}
|
|
126
|
+
return dir_path.split('/').includes(dir_key);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* -------- 公开 API -------- */
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 扫描目录(重复调用只生效一次)
|
|
133
|
+
* @param {string} dir
|
|
134
|
+
* @param {{ extensions?: string[], ignore?: string[] }} [options]
|
|
135
|
+
*/
|
|
136
|
+
scan = (dir, options = {}) => {
|
|
137
|
+
const resolved = path.resolve(dir);
|
|
138
|
+
|
|
139
|
+
if (this.#scanned) {
|
|
140
|
+
if (this.#baseDir !== resolved) {
|
|
141
|
+
console.warn(`[scanner] 已使用 ${this.#baseDir} 扫描过,忽略本次 scan(${resolved})`);
|
|
142
|
+
}
|
|
143
|
+
return this;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!fs.existsSync(resolved)) throw new Error(`[scanner] 目录不存在: ${resolved}`);
|
|
147
|
+
if (!fs.statSync(resolved).isDirectory()) throw new Error(`[scanner] 不是目录: ${resolved}`);
|
|
148
|
+
|
|
149
|
+
if (Array.isArray(options.extensions) || Array.isArray(options.ignore)) {
|
|
150
|
+
this.config = createConfig(this.config, options);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
this.#baseDir = resolved;
|
|
154
|
+
this.#walk(resolved);
|
|
155
|
+
this.#scanned = true;
|
|
156
|
+
return this;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/** 拿条目元信息(含 absPath),key 可带可不带扩展名 */
|
|
160
|
+
get = (key) => {
|
|
161
|
+
this.#ensureReady();
|
|
162
|
+
const k = this.#resolveKey(key);
|
|
163
|
+
return k ? this.#files.get(k) : undefined;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/** 拿绝对路径(给 Loader 用的最常见接口) */
|
|
167
|
+
getPath = (key) => {
|
|
168
|
+
const info = this.get(key);
|
|
169
|
+
return info ? info.absPath : undefined;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
has = (key) => {
|
|
173
|
+
this.#ensureReady();
|
|
174
|
+
return this.#resolveKey(key) !== null;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
keys = () => {
|
|
178
|
+
this.#ensureReady();
|
|
179
|
+
return [...this.#files.keys()];
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
getAll = () => {
|
|
183
|
+
this.#ensureReady();
|
|
184
|
+
return [...this.#files.values()];
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/** 按"扩展名类型"筛:getByExt('.json') / getByExt('json') 都行 */
|
|
188
|
+
getByExt = (ext) => {
|
|
189
|
+
this.#ensureReady();
|
|
190
|
+
const e = normalizeExt(ext);
|
|
191
|
+
return this.getAll().filter((f) => f.ext === e);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/** 所有出现过的扩展名类型 */
|
|
195
|
+
extTypes = () => {
|
|
196
|
+
this.#ensureReady();
|
|
197
|
+
const set = new Set();
|
|
198
|
+
for (const f of this.#files.values()) set.add(f.ext);
|
|
199
|
+
return [...set];
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/** 按目录筛:getByDir('config') -> data/config/**.{json,yaml,...} */
|
|
203
|
+
getByDir = (dir) => {
|
|
204
|
+
this.#ensureReady();
|
|
205
|
+
const norm = normalizeKey(dir);
|
|
206
|
+
const prefix = norm ? norm + '/' : '';
|
|
207
|
+
return this.getAll().filter((f) => f.key.startsWith(prefix));
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 判断文件是否命中指定类型
|
|
212
|
+
* 只写扩展名('.json' / 'json')按扩展名匹配,写文件名('event.json')按文件名结尾匹配
|
|
213
|
+
* @param {{ key: string, ext: string }} file - 文件条目
|
|
214
|
+
* @param {string} ext - 扩展名或文件名
|
|
215
|
+
* @returns {boolean} 命中返回 true
|
|
216
|
+
*/
|
|
217
|
+
#matchExt(file, ext) {
|
|
218
|
+
const norm = String(ext).trim().toLowerCase();
|
|
219
|
+
const bare = norm.replace(/^\./, '');
|
|
220
|
+
if (!bare.includes('.')) return file.ext === '.' + bare;
|
|
221
|
+
return file.key === bare || file.key.endsWith('/' + bare);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* 按目录名与文件类型筛选文件
|
|
226
|
+
* 目录参数可为目录名(匹配任意层级)或相对路径(匹配路径结尾)
|
|
227
|
+
* @param {string} dir - 目录名或相对目录,如 'event_api'、'ai/event_api'
|
|
228
|
+
* @param {string} ext - 扩展名或文件名,如 '.json'、'event.json'
|
|
229
|
+
* @returns {Array<{ key: string, absPath: string, relPath: string, dir: string, name: string, ext: string }>} 命中的文件条目
|
|
230
|
+
*/
|
|
231
|
+
getFileByDir = (dir, ext) => {
|
|
232
|
+
this.#ensureReady();
|
|
233
|
+
const dir_key = normalizeKey(dir);
|
|
234
|
+
return this.getAll().filter((f) => this.#matchDir(f.dir, dir_key) && this.#matchExt(f, ext));
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/** 所有一级子目录名(可当作业务分类) */
|
|
238
|
+
dirTypes = () => {
|
|
239
|
+
this.#ensureReady();
|
|
240
|
+
const set = new Set();
|
|
241
|
+
for (const f of this.#files.values()) {
|
|
242
|
+
const first = f.dir.split('/')[0];
|
|
243
|
+
if (first) set.add(first);
|
|
244
|
+
}
|
|
245
|
+
return [...set];
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/** 当前支持的扩展名列表 */
|
|
249
|
+
get extensions() { return [...this.config.extensions]; }
|
|
250
|
+
|
|
251
|
+
/** 重新扫描(开发环境热更新用) */
|
|
252
|
+
rescan = () => {
|
|
253
|
+
if (!this.#baseDir) throw new Error('[scanner] 尚未初始化,无法 rescan');
|
|
254
|
+
this.#files.clear();
|
|
255
|
+
this.#walk(this.#baseDir);
|
|
256
|
+
return this;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
get size() { return this.#files.size; }
|
|
260
|
+
get baseDir() { return this.#baseDir; }
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/* 导出单例 + 类 */
|
|
264
|
+
module.exports = Scanner;
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mm_scanner",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "目录扫描器,按扩展名递归扫描目录并缓存文件元信息,支持按 key、扩展名、目录检索文件路径。",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://gitee.com/qiuwenwu91/mm_scanner.git"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"mm_mod",
|
|
11
|
+
"mm_infra",
|
|
12
|
+
"mm_common",
|
|
13
|
+
"超级美眉",
|
|
14
|
+
"目录扫描",
|
|
15
|
+
"文件检索",
|
|
16
|
+
"scanner"
|
|
17
|
+
],
|
|
18
|
+
"layer": "infra",
|
|
19
|
+
"group": "common",
|
|
20
|
+
"author": "邱文武",
|
|
21
|
+
"license": "ISC",
|
|
22
|
+
"main": "index.js",
|
|
23
|
+
"files": [
|
|
24
|
+
"index.js",
|
|
25
|
+
"lib",
|
|
26
|
+
"example"
|
|
27
|
+
],
|
|
28
|
+
"directories": {
|
|
29
|
+
"example": "example",
|
|
30
|
+
"lib": "lib"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"test": "jest --coverage",
|
|
34
|
+
"demo": "node example/demo.js",
|
|
35
|
+
"prepublishOnly": "npm test"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"mm_corejs": "^1.1.5"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"jest": "^30.5.2"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/README.md
DELETED