befly-shared 1.1.1
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/LICENSE +201 -0
- package/README.md +442 -0
- package/dist/addonHelper.js +83 -0
- package/dist/arrayKeysToCamel.js +18 -0
- package/dist/arrayToTree.js +23 -0
- package/dist/calcPerfTime.js +13 -0
- package/dist/configTypes.js +1 -0
- package/dist/defineAddonConfig.js +40 -0
- package/dist/fieldClear.js +57 -0
- package/dist/genShortId.js +12 -0
- package/dist/index.js +25 -0
- package/dist/keysToCamel.js +21 -0
- package/dist/keysToSnake.js +21 -0
- package/dist/layouts.js +59 -0
- package/dist/pickFields.js +16 -0
- package/dist/redisKeys.js +34 -0
- package/dist/regex.js +200 -0
- package/dist/scanConfig.js +82 -0
- package/dist/scanFiles.js +39 -0
- package/dist/scanViews.js +48 -0
- package/dist/types.js +46 -0
- package/package.json +40 -0
- package/src/addonHelper.ts +88 -0
- package/src/arrayKeysToCamel.ts +18 -0
- package/src/arrayToTree.ts +31 -0
- package/src/calcPerfTime.ts +13 -0
- package/src/configTypes.ts +27 -0
- package/src/defineAddonConfig.ts +45 -0
- package/src/fieldClear.ts +75 -0
- package/src/genShortId.ts +12 -0
- package/src/index.ts +28 -0
- package/src/keysToCamel.ts +22 -0
- package/src/keysToSnake.ts +22 -0
- package/src/layouts.ts +90 -0
- package/src/pickFields.ts +19 -0
- package/src/redisKeys.ts +44 -0
- package/src/regex.ts +223 -0
- package/src/scanConfig.ts +104 -0
- package/src/scanFiles.ts +49 -0
- package/src/scanViews.ts +55 -0
- package/src/types.ts +338 -0
- package/tests/addonHelper.test.ts +55 -0
- package/tests/arrayKeysToCamel.test.ts +21 -0
- package/tests/arrayToTree.test.ts +98 -0
- package/tests/calcPerfTime.test.ts +19 -0
- package/tests/fieldClear.test.ts +39 -0
- package/tests/keysToCamel.test.ts +22 -0
- package/tests/keysToSnake.test.ts +22 -0
- package/tests/layouts.test.ts +93 -0
- package/tests/pickFields.test.ts +22 -0
- package/tests/regex.test.ts +308 -0
- package/tests/scanFiles.test.ts +58 -0
- package/tests/types.test.ts +283 -0
- package/types/addonConfigMerge.d.ts +17 -0
- package/types/addonHelper.d.ts +24 -0
- package/types/arrayKeysToCamel.d.ts +13 -0
- package/types/arrayToTree.d.ts +8 -0
- package/types/calcPerfTime.d.ts +4 -0
- package/types/configMerge.d.ts +49 -0
- package/types/configTypes.d.ts +26 -0
- package/types/defineAddonConfig.d.ts +19 -0
- package/types/fieldClear.d.ts +16 -0
- package/types/genShortId.d.ts +10 -0
- package/types/index.d.ts +22 -0
- package/types/keysToCamel.d.ts +10 -0
- package/types/keysToSnake.d.ts +10 -0
- package/types/layouts.d.ts +29 -0
- package/types/loadAndMergeConfig.d.ts +7 -0
- package/types/mergeConfig.d.ts +7 -0
- package/types/pickFields.d.ts +4 -0
- package/types/redisKeys.d.ts +34 -0
- package/types/regex.d.ts +143 -0
- package/types/scanConfig.d.ts +7 -0
- package/types/scanFiles.d.ts +12 -0
- package/types/scanViews.d.ts +11 -0
- package/types/types.d.ts +274 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, join } from 'node:path';
|
|
3
|
+
import { mergeAndConcat } from 'merge-anything';
|
|
4
|
+
import { isPlainObject } from 'es-toolkit';
|
|
5
|
+
import { get, set } from 'es-toolkit/compat';
|
|
6
|
+
/**
|
|
7
|
+
* 扫描并合并配置文件(矩阵搜索:dirs × files)
|
|
8
|
+
* @param options - 加载选项
|
|
9
|
+
* @returns 合并后的配置对象(或第一个找到的配置)
|
|
10
|
+
*/
|
|
11
|
+
export async function scanConfig(options) {
|
|
12
|
+
const {
|
|
13
|
+
//
|
|
14
|
+
cwd = process.cwd(), dirs, files, extensions = ['.js', '.ts', '.json'], mode = 'first', paths } = options;
|
|
15
|
+
// 参数验证
|
|
16
|
+
if (!Array.isArray(dirs) || dirs.length === 0) {
|
|
17
|
+
throw new Error('dirs 必须是非空数组');
|
|
18
|
+
}
|
|
19
|
+
if (!Array.isArray(files) || files.length === 0) {
|
|
20
|
+
throw new Error('files 必须是非空数组');
|
|
21
|
+
}
|
|
22
|
+
const configs = [];
|
|
23
|
+
// 矩阵搜索:dirs × files × extensions
|
|
24
|
+
for (const dir of dirs) {
|
|
25
|
+
// 如果是绝对路径则直接使用,否则拼接 cwd
|
|
26
|
+
const fullDir = isAbsolute(dir) ? dir : join(cwd, dir);
|
|
27
|
+
for (const file of files) {
|
|
28
|
+
for (const ext of extensions) {
|
|
29
|
+
const fileName = file.endsWith(ext) ? file : file + ext;
|
|
30
|
+
const filePath = join(fullDir, fileName);
|
|
31
|
+
if (existsSync(filePath)) {
|
|
32
|
+
try {
|
|
33
|
+
// 动态导入配置文件(使用 import 断言处理 JSON)
|
|
34
|
+
let data;
|
|
35
|
+
if (ext === '.json') {
|
|
36
|
+
// JSON 文件使用 import 断言
|
|
37
|
+
const module = await import(filePath, { with: { type: 'json' } });
|
|
38
|
+
data = module.default;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
// JS/TS 文件使用动态导入
|
|
42
|
+
const module = await import(filePath);
|
|
43
|
+
data = module.default || module;
|
|
44
|
+
// 处理 async 函数导出(如 defineAddonConfig)
|
|
45
|
+
if (data instanceof Promise) {
|
|
46
|
+
data = await data;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// 验证配置数据
|
|
50
|
+
if (!isPlainObject(data)) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
configs.push(data);
|
|
54
|
+
// 如果模式为 'first',找到第一个配置后立即返回
|
|
55
|
+
if (mode === 'first') {
|
|
56
|
+
return data;
|
|
57
|
+
}
|
|
58
|
+
// 找到后跳过同名文件的其他扩展名
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
console.error(`加载配置文件失败: ${filePath}`, error.message);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// 合并配置(使用 mergeAndConcat 深度合并)
|
|
69
|
+
const finalConfig = configs.length > 0 ? mergeAndConcat({}, ...configs) : {};
|
|
70
|
+
// 如果指定了 paths,则只返回指定路径的字段
|
|
71
|
+
if (paths && paths.length > 0) {
|
|
72
|
+
const result = {};
|
|
73
|
+
for (const path of paths) {
|
|
74
|
+
const value = get(finalConfig, path);
|
|
75
|
+
if (value !== undefined) {
|
|
76
|
+
set(result, path, value);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
return finalConfig;
|
|
82
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { relative, basename, normalize } from 'pathe';
|
|
3
|
+
/**
|
|
4
|
+
* 扫描指定目录下的文件
|
|
5
|
+
* @param dir 目录路径
|
|
6
|
+
* @param pattern Glob 模式
|
|
7
|
+
* @param ignoreUnderline 是否忽略下划线开头的文件/目录
|
|
8
|
+
*/
|
|
9
|
+
export async function scanFiles(dir, pattern = '**/*.{ts,js}', ignoreUnderline = true) {
|
|
10
|
+
if (!existsSync(dir))
|
|
11
|
+
return [];
|
|
12
|
+
const normalizedDir = normalize(dir);
|
|
13
|
+
const glob = new Bun.Glob(pattern);
|
|
14
|
+
const results = [];
|
|
15
|
+
for await (const file of glob.scan({ cwd: dir, onlyFiles: true, absolute: true })) {
|
|
16
|
+
if (file.endsWith('.d.ts'))
|
|
17
|
+
continue;
|
|
18
|
+
// 使用 pathe.normalize 统一路径分隔符为 /
|
|
19
|
+
const normalizedFile = normalize(file);
|
|
20
|
+
// 获取文件名(去除扩展名)
|
|
21
|
+
const fileName = basename(normalizedFile).replace(/\.[^.]+$/, '');
|
|
22
|
+
// 计算相对路径(pathe.relative 返回的已经是正斜杠路径)
|
|
23
|
+
const relativePath = relative(normalizedDir, normalizedFile).replace(/\.[^/.]+$/, '');
|
|
24
|
+
if (ignoreUnderline) {
|
|
25
|
+
// 检查文件名是否以下划线开头
|
|
26
|
+
if (fileName.startsWith('_'))
|
|
27
|
+
continue;
|
|
28
|
+
// 检查路径中是否包含下划线开头的目录
|
|
29
|
+
if (relativePath.split('/').some((part) => part.startsWith('_')))
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
results.push({
|
|
33
|
+
filePath: normalizedFile,
|
|
34
|
+
relativePath,
|
|
35
|
+
fileName
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return results;
|
|
39
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readdirSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* 扫描项目和所有 @befly-addon 包的 views 目录
|
|
5
|
+
* 用于 unplugin-vue-router 的 routesFolder 配置
|
|
6
|
+
* 注意:此函数只能在 vite.config.js 中使用(Node.js 环境),不能在浏览器中使用
|
|
7
|
+
* @returns 路由文件夹配置数组
|
|
8
|
+
*/
|
|
9
|
+
export function scanViews() {
|
|
10
|
+
// 使用绝对路径:基于项目根目录(process.cwd())
|
|
11
|
+
const projectRoot = process.cwd();
|
|
12
|
+
const addonBasePath = join(projectRoot, 'node_modules', '@befly-addon');
|
|
13
|
+
const routesFolders = [];
|
|
14
|
+
// 1. 先添加项目自己的 views 目录
|
|
15
|
+
const projectViewsPath = join(projectRoot, 'src', 'views');
|
|
16
|
+
if (existsSync(projectViewsPath)) {
|
|
17
|
+
routesFolders.push({
|
|
18
|
+
src: projectViewsPath,
|
|
19
|
+
path: '',
|
|
20
|
+
exclude: ['**/components/**']
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
// 2. 扫描 @befly-addon 包的 views 目录
|
|
24
|
+
if (!existsSync(addonBasePath)) {
|
|
25
|
+
return routesFolders;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const addonDirs = readdirSync(addonBasePath);
|
|
29
|
+
for (const addonName of addonDirs) {
|
|
30
|
+
const addonPath = join(addonBasePath, addonName);
|
|
31
|
+
// 检查是否为目录(包括符号链接)
|
|
32
|
+
if (!existsSync(addonPath))
|
|
33
|
+
continue;
|
|
34
|
+
const viewsPath = join(addonPath, 'views');
|
|
35
|
+
if (existsSync(viewsPath)) {
|
|
36
|
+
routesFolders.push({
|
|
37
|
+
src: viewsPath,
|
|
38
|
+
path: `addon/${addonName}/`,
|
|
39
|
+
exclude: ['**/components/**']
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
console.error('扫描 @befly-addon 目录失败:', error);
|
|
46
|
+
}
|
|
47
|
+
return routesFolders;
|
|
48
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Befly 共享类型定义
|
|
3
|
+
* 这些类型可以在 core、tpl、admin 等多个包中复用
|
|
4
|
+
*/
|
|
5
|
+
// ============================================
|
|
6
|
+
// API 响应码常量
|
|
7
|
+
// ============================================
|
|
8
|
+
/**
|
|
9
|
+
* API 响应码
|
|
10
|
+
*/
|
|
11
|
+
export const ApiCode = {
|
|
12
|
+
/** 成功 */
|
|
13
|
+
SUCCESS: 0,
|
|
14
|
+
/** 通用失败 */
|
|
15
|
+
FAIL: 1,
|
|
16
|
+
/** 未授权 */
|
|
17
|
+
UNAUTHORIZED: 401,
|
|
18
|
+
/** 禁止访问 */
|
|
19
|
+
FORBIDDEN: 403,
|
|
20
|
+
/** 未找到 */
|
|
21
|
+
NOT_FOUND: 404,
|
|
22
|
+
/** 服务器错误 */
|
|
23
|
+
SERVER_ERROR: 500
|
|
24
|
+
};
|
|
25
|
+
// ============================================
|
|
26
|
+
// 错误消息常量
|
|
27
|
+
// ============================================
|
|
28
|
+
/**
|
|
29
|
+
* 通用错误消息
|
|
30
|
+
*/
|
|
31
|
+
export const ErrorMessages = {
|
|
32
|
+
/** 未授权 */
|
|
33
|
+
UNAUTHORIZED: '请先登录',
|
|
34
|
+
/** 禁止访问 */
|
|
35
|
+
FORBIDDEN: '无访问权限',
|
|
36
|
+
/** 未找到 */
|
|
37
|
+
NOT_FOUND: '资源不存在',
|
|
38
|
+
/** 服务器错误 */
|
|
39
|
+
SERVER_ERROR: '服务器错误',
|
|
40
|
+
/** 参数错误 */
|
|
41
|
+
INVALID_PARAMS: '参数错误',
|
|
42
|
+
/** Token 过期 */
|
|
43
|
+
TOKEN_EXPIRED: 'Token 已过期',
|
|
44
|
+
/** Token 无效 */
|
|
45
|
+
TOKEN_INVALID: 'Token 无效'
|
|
46
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "befly-shared",
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"bun": "./src/index.ts",
|
|
8
|
+
"import": "./dist/index.js",
|
|
9
|
+
"types": "./types/index.d.ts"
|
|
10
|
+
},
|
|
11
|
+
"./*": {
|
|
12
|
+
"bun": "./src/*.ts",
|
|
13
|
+
"import": "./dist/*.js",
|
|
14
|
+
"types": "./types/*.d.ts"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"src",
|
|
20
|
+
"types",
|
|
21
|
+
"tests",
|
|
22
|
+
"package.json",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "rimraf dist && tsc",
|
|
27
|
+
"test": "bun test"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/bun": "latest",
|
|
31
|
+
"rimraf": "^6.1.2",
|
|
32
|
+
"typescript": "^5.7.2"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"es-toolkit": "^1.42.0",
|
|
36
|
+
"merge-anything": "^6.0.6",
|
|
37
|
+
"pathe": "^2.0.3"
|
|
38
|
+
},
|
|
39
|
+
"gitHead": "2031550167896390ac58c50aeb52b8c23426844e"
|
|
40
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { join } from 'pathe';
|
|
3
|
+
import { statSync, existsSync } from 'node:fs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 扫描所有可用的 addon
|
|
7
|
+
* 优先从本地 addons/ 目录加载,其次从 node_modules/@befly-addon/ 加载
|
|
8
|
+
* @param cwd - 项目根目录,默认为 process.cwd()
|
|
9
|
+
* @returns addon 名称数组
|
|
10
|
+
*/
|
|
11
|
+
export const scanAddons = (cwd: string = process.cwd()): string[] => {
|
|
12
|
+
const addons = new Set<string>();
|
|
13
|
+
// const projectAddonsDir = join(cwd, 'addons');
|
|
14
|
+
|
|
15
|
+
// 1. 扫描本地 addons 目录(优先级高)
|
|
16
|
+
// if (existsSync(projectAddonsDir)) {
|
|
17
|
+
// try {
|
|
18
|
+
// const localAddons = fs.readdirSync(projectAddonsDir).filter((name) => {
|
|
19
|
+
// const fullPath = join(projectAddonsDir, name);
|
|
20
|
+
// try {
|
|
21
|
+
// const stat = statSync(fullPath);
|
|
22
|
+
// return stat.isDirectory() && !name.startsWith('_');
|
|
23
|
+
// } catch {
|
|
24
|
+
// return false;
|
|
25
|
+
// }
|
|
26
|
+
// });
|
|
27
|
+
// localAddons.forEach((name) => addons.add(name));
|
|
28
|
+
// } catch (err) {
|
|
29
|
+
// // 忽略本地目录读取错误
|
|
30
|
+
// }
|
|
31
|
+
// }
|
|
32
|
+
|
|
33
|
+
// 2. 扫描 node_modules/@befly-addon 目录
|
|
34
|
+
const beflyDir = join(cwd, 'node_modules', '@befly-addon');
|
|
35
|
+
if (existsSync(beflyDir)) {
|
|
36
|
+
try {
|
|
37
|
+
const npmAddons = fs.readdirSync(beflyDir).filter((name) => {
|
|
38
|
+
// 如果本地已存在,跳过 npm 包版本
|
|
39
|
+
if (addons.has(name)) return false;
|
|
40
|
+
|
|
41
|
+
const fullPath = join(beflyDir, name);
|
|
42
|
+
try {
|
|
43
|
+
const stat = statSync(fullPath);
|
|
44
|
+
return stat.isDirectory();
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
npmAddons.forEach((name) => addons.add(name));
|
|
50
|
+
} catch {
|
|
51
|
+
// 忽略 npm 目录读取错误
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return Array.from(addons).sort();
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 获取 addon 的指定子目录路径
|
|
60
|
+
* 优先返回本地 addons 目录,其次返回 node_modules 目录
|
|
61
|
+
* @param name - addon 名称
|
|
62
|
+
* @param subDir - 子目录名称
|
|
63
|
+
* @param cwd - 项目根目录,默认为 process.cwd()
|
|
64
|
+
* @returns 完整路径
|
|
65
|
+
*/
|
|
66
|
+
export const getAddonDir = (name: string, subDir: string, cwd: string = process.cwd()): string => {
|
|
67
|
+
// 优先使用本地 addons 目录
|
|
68
|
+
// const projectAddonsDir = join(cwd, 'addons');
|
|
69
|
+
// const localPath = join(projectAddonsDir, name, subDir);
|
|
70
|
+
// if (existsSync(localPath)) {
|
|
71
|
+
// return localPath;
|
|
72
|
+
// }
|
|
73
|
+
|
|
74
|
+
// 降级使用 node_modules 目录
|
|
75
|
+
return join(cwd, 'node_modules', '@befly-addon', name, subDir);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 检查 addon 子目录是否存在
|
|
80
|
+
* @param name - addon 名称
|
|
81
|
+
* @param subDir - 子目录名称
|
|
82
|
+
* @param cwd - 项目根目录,默认为 process.cwd()
|
|
83
|
+
* @returns 是否存在
|
|
84
|
+
*/
|
|
85
|
+
export const addonDirExists = (name: string, subDir: string, cwd: string = process.cwd()): boolean => {
|
|
86
|
+
const dir = getAddonDir(name, subDir, cwd);
|
|
87
|
+
return existsSync(dir) && statSync(dir).isDirectory();
|
|
88
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { keysToCamel } from './keysToCamel.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 数组对象字段名批量转小驼峰
|
|
5
|
+
* @param arr - 源数组
|
|
6
|
+
* @returns 字段名转为小驼峰格式的新数组
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* arrayKeysToCamel([
|
|
10
|
+
* { user_id: 1, user_name: 'John' },
|
|
11
|
+
* { user_id: 2, user_name: 'Jane' }
|
|
12
|
+
* ])
|
|
13
|
+
* // [{ userId: 1, userName: 'John' }, { userId: 2, userName: 'Jane' }]
|
|
14
|
+
*/
|
|
15
|
+
export const arrayKeysToCamel = <T = any>(arr: Record<string, any>[]): T[] => {
|
|
16
|
+
if (!arr || !Array.isArray(arr)) return arr as T[];
|
|
17
|
+
return arr.map((item) => keysToCamel<T>(item));
|
|
18
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// arrayToTree 工具函数实现
|
|
2
|
+
|
|
3
|
+
export interface ArrayToTreeOptions<T = any> {
|
|
4
|
+
idField?: string; // 节点 id 字段名,默认 'id'
|
|
5
|
+
pidField?: string; // 父节点 id 字段名,默认 'pid'
|
|
6
|
+
childrenField?: string; // 子节点字段名,默认 'children'
|
|
7
|
+
rootPid?: any; // 根节点的父 id,默认 0
|
|
8
|
+
mapFn?: (node: T) => T; // 节点转换函数
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function arrayToTree<T = any>(items: T[], options: ArrayToTreeOptions<T> = {}): T[] {
|
|
12
|
+
const { idField = 'id', pidField = 'pid', childrenField = 'children', rootPid = 0, mapFn } = options;
|
|
13
|
+
const tree: T[] = [];
|
|
14
|
+
for (const item of items) {
|
|
15
|
+
const pid = (item as any)[pidField];
|
|
16
|
+
// 用 Object.is 判断,兼容 null/undefined/0
|
|
17
|
+
if (Object.is(pid, rootPid)) {
|
|
18
|
+
let node = { ...item };
|
|
19
|
+
if (mapFn) node = mapFn(node);
|
|
20
|
+
const children = arrayToTree(items, {
|
|
21
|
+
...options,
|
|
22
|
+
rootPid: (node as any)[idField]
|
|
23
|
+
});
|
|
24
|
+
if (children.length > 0) {
|
|
25
|
+
(node as any)[childrenField] = children;
|
|
26
|
+
}
|
|
27
|
+
tree.push(node);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return tree;
|
|
31
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 计算性能时间差
|
|
3
|
+
*/
|
|
4
|
+
export const calcPerfTime = (startTime: number, endTime: number = Bun.nanoseconds()): string => {
|
|
5
|
+
const elapsedMs = (endTime - startTime) / 1_000_000;
|
|
6
|
+
|
|
7
|
+
if (elapsedMs < 1000) {
|
|
8
|
+
return `${elapsedMs.toFixed(2)} 毫秒`;
|
|
9
|
+
} else {
|
|
10
|
+
const elapsedSeconds = elapsedMs / 1000;
|
|
11
|
+
return `${elapsedSeconds.toFixed(2)} 秒`;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 加载配置选项
|
|
3
|
+
*/
|
|
4
|
+
export interface LoadConfigOptions {
|
|
5
|
+
/** 当前工作目录,默认 process.cwd() */
|
|
6
|
+
cwd?: string;
|
|
7
|
+
/** 目录数组:要搜索的目录路径(相对于 cwd) */
|
|
8
|
+
dirs: string[];
|
|
9
|
+
/** 文件数组:要匹配的文件名 */
|
|
10
|
+
files: string[];
|
|
11
|
+
/** 文件扩展名,默认 ['.js', '.ts', '.json'] */
|
|
12
|
+
extensions?: string[];
|
|
13
|
+
/** 加载模式:'first' = 返回第一个找到的配置(默认),'all' = 合并所有配置 */
|
|
14
|
+
mode?: 'all' | 'first';
|
|
15
|
+
/** 指定要提取的字段路径数组,如 ['menus', 'database.host'],为空则返回完整对象 */
|
|
16
|
+
paths?: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Addon 配置合并选项(已废弃,仅用于向后兼容)
|
|
21
|
+
*/
|
|
22
|
+
export interface MergeConfigOptions {
|
|
23
|
+
/** 合并策略(已废弃,始终使用 deep) */
|
|
24
|
+
strategy?: 'deep' | 'shallow' | 'override';
|
|
25
|
+
/** 是否克隆数据(已废弃) */
|
|
26
|
+
clone?: boolean;
|
|
27
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { join, basename } from 'pathe';
|
|
2
|
+
|
|
3
|
+
import { mergeAndConcat } from 'merge-anything';
|
|
4
|
+
|
|
5
|
+
import { scanConfig } from './scanConfig.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Addon 配置定义函数
|
|
9
|
+
* 接受 addon 配置对象,从当前执行目录的 config 目录下查找同名配置文件进行合并
|
|
10
|
+
* 只能在 addon.config.js 文件中使用
|
|
11
|
+
* @param metaDirname - import.meta.dirname
|
|
12
|
+
* @param addonConfig - addon 配置对象
|
|
13
|
+
* @returns 合并后的配置对象
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* // 在 packages/addonAdmin/addon.config.js 中
|
|
17
|
+
* import { defineAddonConfig } from 'befly-shared/defineAddonConfig';
|
|
18
|
+
*
|
|
19
|
+
* export default defineAddonConfig(import.meta.dirname, {
|
|
20
|
+
* menus: [...]
|
|
21
|
+
* });
|
|
22
|
+
* // 自动从目录名提取 addon 名称,并从 process.cwd()/config/addonAdmin.{js,ts,json} 读取配置并合并
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export async function defineAddonConfig(metaDirname: string, addonConfig: Record<string, any> = {}): Promise<Record<string, any>> {
|
|
26
|
+
try {
|
|
27
|
+
// 1. 使用 pathe 的 basename 获取完整的目录名(保留 addon 前缀)
|
|
28
|
+
const addonName = basename(metaDirname);
|
|
29
|
+
|
|
30
|
+
// 2. 从当前执行目录的 config 目录查找配置
|
|
31
|
+
const projectConfigDir = join(process.cwd(), 'config');
|
|
32
|
+
|
|
33
|
+
// 3. 使用 scanConfig 加载项目配置
|
|
34
|
+
const projectConfig = await scanConfig({
|
|
35
|
+
dirs: [projectConfigDir],
|
|
36
|
+
files: [addonName]
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// 4. 合并 addon 配置和项目配置(项目配置优先级更高)
|
|
40
|
+
return mergeAndConcat({}, addonConfig, projectConfig);
|
|
41
|
+
} catch (error: any) {
|
|
42
|
+
console.error('defineAddonConfig 失败:', error.message);
|
|
43
|
+
return addonConfig;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// fieldClear 工具函数实现
|
|
2
|
+
// 支持 pick/omit/keepValues/excludeValues,处理对象和数组
|
|
3
|
+
|
|
4
|
+
export interface FieldClearOptions {
|
|
5
|
+
pickKeys?: string[]; // 只保留这些字段
|
|
6
|
+
omitKeys?: string[]; // 排除这些字段
|
|
7
|
+
keepValues?: any[]; // 只保留这些值
|
|
8
|
+
excludeValues?: any[]; // 排除这些值
|
|
9
|
+
keepMap?: Record<string, any>; // 强制保留的键值对(优先级最高,忽略 excludeValues)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type FieldClearResult<T> = T extends Array<infer U> ? Array<FieldClearResult<U>> : T extends object ? { [K in keyof T]?: T[K] } : T;
|
|
13
|
+
|
|
14
|
+
function isObject(val: unknown): val is Record<string, any> {
|
|
15
|
+
return val !== null && typeof val === 'object' && !Array.isArray(val);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isArray(val: unknown): val is any[] {
|
|
19
|
+
return Array.isArray(val);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function fieldClear<T = any>(data: T | T[], options: FieldClearOptions = {}): FieldClearResult<T> {
|
|
23
|
+
const { pickKeys, omitKeys, keepValues, excludeValues, keepMap } = options;
|
|
24
|
+
|
|
25
|
+
const filterObj = (obj: Record<string, any>) => {
|
|
26
|
+
let result: Record<string, any> = {};
|
|
27
|
+
let keys = Object.keys(obj);
|
|
28
|
+
if (pickKeys && pickKeys.length) {
|
|
29
|
+
keys = keys.filter((k) => pickKeys.includes(k));
|
|
30
|
+
}
|
|
31
|
+
if (omitKeys && omitKeys.length) {
|
|
32
|
+
keys = keys.filter((k) => !omitKeys.includes(k));
|
|
33
|
+
}
|
|
34
|
+
for (const key of keys) {
|
|
35
|
+
const value = obj[key];
|
|
36
|
+
|
|
37
|
+
// 1. 优先检查 keepMap
|
|
38
|
+
if (keepMap && key in keepMap) {
|
|
39
|
+
if (Object.is(keepMap[key], value)) {
|
|
40
|
+
result[key] = value;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 2. 检查 keepValues (只保留指定值)
|
|
46
|
+
if (keepValues && keepValues.length && !keepValues.includes(value)) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 3. 检查 excludeValues (排除指定值)
|
|
51
|
+
if (excludeValues && excludeValues.length && excludeValues.includes(value)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
result[key] = value;
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if (isArray(data)) {
|
|
60
|
+
return (data as any[])
|
|
61
|
+
.map((item) => (isObject(item) ? filterObj(item) : item))
|
|
62
|
+
.filter((item) => {
|
|
63
|
+
if (isObject(item)) {
|
|
64
|
+
// 只保留有内容的对象
|
|
65
|
+
return Object.keys(item).length > 0;
|
|
66
|
+
}
|
|
67
|
+
// 原始值直接保留
|
|
68
|
+
return true;
|
|
69
|
+
}) as FieldClearResult<T>;
|
|
70
|
+
}
|
|
71
|
+
if (isObject(data)) {
|
|
72
|
+
return filterObj(data as Record<string, any>) as FieldClearResult<T>;
|
|
73
|
+
}
|
|
74
|
+
return data as FieldClearResult<T>;
|
|
75
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 生成短 ID
|
|
3
|
+
* 由时间戳(base36)+ 随机字符组成,约 13 位
|
|
4
|
+
* - 前 8 位:时间戳(可排序)
|
|
5
|
+
* - 后 5 位:随机字符(防冲突)
|
|
6
|
+
* @returns 短 ID 字符串
|
|
7
|
+
* @example
|
|
8
|
+
* genShortId() // "lxyz1a2b3c4"
|
|
9
|
+
*/
|
|
10
|
+
export function genShortId(): string {
|
|
11
|
+
return Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
|
|
12
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Befly Shared 统一导出
|
|
3
|
+
* 跨包共享的工具函数、常量、类型定义
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// 类型定义
|
|
7
|
+
export * from './types.js';
|
|
8
|
+
|
|
9
|
+
// 常量配置
|
|
10
|
+
export * from './redisKeys.js';
|
|
11
|
+
export * from './regex.js';
|
|
12
|
+
|
|
13
|
+
// 工具函数
|
|
14
|
+
export * from './defineAddonConfig.js';
|
|
15
|
+
export * from './addonHelper.js';
|
|
16
|
+
export * from './arrayKeysToCamel.js';
|
|
17
|
+
export * from './arrayToTree.js';
|
|
18
|
+
export * from './calcPerfTime.js';
|
|
19
|
+
export * from './configTypes.js';
|
|
20
|
+
export * from './genShortId.js';
|
|
21
|
+
export * from './scanConfig.js';
|
|
22
|
+
export * from './fieldClear.js';
|
|
23
|
+
export * from './keysToCamel.js';
|
|
24
|
+
export * from './keysToSnake.js';
|
|
25
|
+
export * from './layouts.js';
|
|
26
|
+
export * from './pickFields.js';
|
|
27
|
+
export * from './scanFiles.js';
|
|
28
|
+
export * from './scanViews.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { isPlainObject } from 'es-toolkit/compat';
|
|
2
|
+
import { camelCase } from 'es-toolkit/string';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 对象字段名转小驼峰
|
|
6
|
+
* @param obj - 源对象
|
|
7
|
+
* @returns 字段名转为小驼峰格式的新对象
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* keysToCamel({ user_id: 123, user_name: 'John' }) // { userId: 123, userName: 'John' }
|
|
11
|
+
* keysToCamel({ created_at: 1697452800000 }) // { createdAt: 1697452800000 }
|
|
12
|
+
*/
|
|
13
|
+
export const keysToCamel = <T = any>(obj: Record<string, any>): T => {
|
|
14
|
+
if (!obj || !isPlainObject(obj)) return obj as T;
|
|
15
|
+
|
|
16
|
+
const result: any = {};
|
|
17
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
18
|
+
const camelKey = camelCase(key);
|
|
19
|
+
result[camelKey] = value;
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { isPlainObject } from 'es-toolkit/compat';
|
|
2
|
+
import { snakeCase } from 'es-toolkit/string';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 对象字段名转下划线
|
|
6
|
+
* @param obj - 源对象
|
|
7
|
+
* @returns 字段名转为下划线格式的新对象
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* keysToSnake({ userId: 123, userName: 'John' }) // { user_id: 123, user_name: 'John' }
|
|
11
|
+
* keysToSnake({ createdAt: 1697452800000 }) // { created_at: 1697452800000 }
|
|
12
|
+
*/
|
|
13
|
+
export const keysToSnake = <T = any>(obj: Record<string, any>): T => {
|
|
14
|
+
if (!obj || !isPlainObject(obj)) return obj as T;
|
|
15
|
+
|
|
16
|
+
const result: any = {};
|
|
17
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
18
|
+
const snakeKey = snakeCase(key);
|
|
19
|
+
result[snakeKey] = value;
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
};
|