befly-shared 1.2.8 → 1.3.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.
Files changed (98) hide show
  1. package/package.json +13 -30
  2. package/utils/arrayToTree.ts +135 -0
  3. package/utils/buildTreeByParentPath.ts +127 -0
  4. package/utils/scanViewsDir.ts +148 -0
  5. package/README.md +0 -439
  6. package/dist/addonHelper.js +0 -83
  7. package/dist/arrayKeysToCamel.js +0 -18
  8. package/dist/arrayToTree.js +0 -23
  9. package/dist/calcPerfTime.js +0 -13
  10. package/dist/configTypes.js +0 -1
  11. package/dist/constants.js +0 -46
  12. package/dist/deepTransformKeys.js +0 -139
  13. package/dist/fieldClear.js +0 -57
  14. package/dist/genShortId.js +0 -12
  15. package/dist/hashPassword.js +0 -22
  16. package/dist/index.js +0 -26
  17. package/dist/keysToCamel.js +0 -21
  18. package/dist/keysToSnake.js +0 -21
  19. package/dist/layouts.js +0 -59
  20. package/dist/pickFields.js +0 -16
  21. package/dist/redisKeys.js +0 -34
  22. package/dist/regex.js +0 -202
  23. package/dist/scanConfig.js +0 -83
  24. package/dist/scanFiles.js +0 -39
  25. package/dist/scanViews.js +0 -48
  26. package/dist/withDefaultColumns.js +0 -32
  27. package/src/addonHelper.ts +0 -88
  28. package/src/arrayKeysToCamel.ts +0 -18
  29. package/src/arrayToTree.ts +0 -31
  30. package/src/calcPerfTime.ts +0 -13
  31. package/src/configTypes.ts +0 -29
  32. package/src/constants.ts +0 -60
  33. package/src/deepTransformKeys.ts +0 -172
  34. package/src/fieldClear.ts +0 -75
  35. package/src/genShortId.ts +0 -12
  36. package/src/hashPassword.ts +0 -27
  37. package/src/index.ts +0 -29
  38. package/src/keysToCamel.ts +0 -22
  39. package/src/keysToSnake.ts +0 -22
  40. package/src/layouts.ts +0 -90
  41. package/src/pickFields.ts +0 -19
  42. package/src/redisKeys.ts +0 -44
  43. package/src/regex.ts +0 -225
  44. package/src/scanConfig.ts +0 -106
  45. package/src/scanFiles.ts +0 -49
  46. package/src/scanViews.ts +0 -55
  47. package/src/withDefaultColumns.ts +0 -36
  48. package/tests/addonHelper.test.ts +0 -55
  49. package/tests/arrayKeysToCamel.test.ts +0 -21
  50. package/tests/arrayToTree.test.ts +0 -98
  51. package/tests/calcPerfTime.test.ts +0 -19
  52. package/tests/deepTransformKeys.test.ts +0 -466
  53. package/tests/fieldClear.test.ts +0 -39
  54. package/tests/keysToCamel.test.ts +0 -22
  55. package/tests/keysToSnake.test.ts +0 -22
  56. package/tests/layouts.test.ts +0 -93
  57. package/tests/pickFields.test.ts +0 -22
  58. package/tests/regex.test.ts +0 -308
  59. package/tests/scanFiles.test.ts +0 -58
  60. package/tests/types.test.ts +0 -289
  61. package/types/addon.d.ts +0 -50
  62. package/types/addonConfigMerge.d.ts +0 -17
  63. package/types/addonHelper.d.ts +0 -24
  64. package/types/api.d.ts +0 -63
  65. package/types/arrayKeysToCamel.d.ts +0 -13
  66. package/types/arrayToTree.d.ts +0 -8
  67. package/types/calcPerfTime.d.ts +0 -4
  68. package/types/common.d.ts +0 -8
  69. package/types/configMerge.d.ts +0 -49
  70. package/types/configTypes.d.ts +0 -28
  71. package/types/constants.d.ts +0 -48
  72. package/types/context.d.ts +0 -38
  73. package/types/crypto.d.ts +0 -23
  74. package/types/database.d.ts +0 -55
  75. package/types/deepTransformKeys.d.ts +0 -84
  76. package/types/fieldClear.d.ts +0 -16
  77. package/types/genShortId.d.ts +0 -10
  78. package/types/hashPassword.d.ts +0 -11
  79. package/types/index.d.ts +0 -23
  80. package/types/jwt.d.ts +0 -99
  81. package/types/keysToCamel.d.ts +0 -10
  82. package/types/keysToSnake.d.ts +0 -10
  83. package/types/layouts.d.ts +0 -29
  84. package/types/loadAndMergeConfig.d.ts +0 -7
  85. package/types/logger.d.ts +0 -22
  86. package/types/menu.d.ts +0 -49
  87. package/types/mergeConfig.d.ts +0 -7
  88. package/types/pickFields.d.ts +0 -4
  89. package/types/redisKeys.d.ts +0 -34
  90. package/types/regex.d.ts +0 -145
  91. package/types/scanConfig.d.ts +0 -7
  92. package/types/scanFiles.d.ts +0 -12
  93. package/types/scanViews.d.ts +0 -11
  94. package/types/table.d.ts +0 -49
  95. package/types/tool.d.ts +0 -67
  96. package/types/types.d.ts +0 -44
  97. package/types/validate.d.ts +0 -69
  98. package/types/withDefaultColumns.d.ts +0 -7
@@ -1,83 +0,0 @@
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, defaults = {} } = 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
- // 合并顺序:defaults ← configs[0] ← configs[1] ← ...
70
- const finalConfig = mergeAndConcat(defaults, ...configs);
71
- // 如果指定了 paths,则只返回指定路径的字段
72
- if (paths && paths.length > 0) {
73
- const result = {};
74
- for (const path of paths) {
75
- const value = get(finalConfig, path);
76
- if (value !== undefined) {
77
- set(result, path, value);
78
- }
79
- }
80
- return result;
81
- }
82
- return finalConfig;
83
- }
package/dist/scanFiles.js DELETED
@@ -1,39 +0,0 @@
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
- }
package/dist/scanViews.js DELETED
@@ -1,48 +0,0 @@
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
- }
@@ -1,32 +0,0 @@
1
- /**
2
- * 为表格列添加默认配置
3
- * @param columns - 列配置数组
4
- * @param customConfig - 自定义特殊列配置,可覆盖默认的 specialColumnConfig
5
- * @returns 添加默认配置后的列数组
6
- */
7
- export function withDefaultColumns(columns, customConfig = {}) {
8
- /**
9
- * 特殊列配置映射
10
- */
11
- const specialColumnConfig = {
12
- operation: { width: 100, align: 'center', fixed: 'right' },
13
- state: { width: 100, align: 'center' },
14
- id: { width: 200, align: 'center' },
15
- ...customConfig
16
- };
17
- return columns.map((col) => {
18
- const colKey = col.colKey;
19
- // 特殊列配置
20
- let specialConfig = specialColumnConfig[colKey];
21
- // 以 At 或 At2 结尾的列(时间字段)
22
- if (!specialConfig && colKey && (colKey.endsWith('At') || colKey.endsWith('At2'))) {
23
- specialConfig = { align: 'center' };
24
- }
25
- return {
26
- width: 200,
27
- ellipsis: true,
28
- ...specialConfig,
29
- ...col
30
- };
31
- });
32
- }
@@ -1,88 +0,0 @@
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
- };
@@ -1,18 +0,0 @@
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
- };
@@ -1,31 +0,0 @@
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
- }
@@ -1,13 +0,0 @@
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
- };
@@ -1,29 +0,0 @@
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' = 返回第一个找到的配置(默认),'merge' = 合并所有配置 */
14
- mode?: 'merge' | 'first';
15
- /** 指定要提取的字段路径数组,如 ['menus', 'database.host'],为空则返回完整对象 */
16
- paths?: string[];
17
- /** 默认配置,会与找到的配置合并(优先级最低) */
18
- defaults?: Record<string, any>;
19
- }
20
-
21
- /**
22
- * Addon 配置合并选项(已废弃,仅用于向后兼容)
23
- */
24
- export interface MergeConfigOptions {
25
- /** 合并策略(已废弃,始终使用 deep) */
26
- strategy?: 'deep' | 'shallow' | 'override';
27
- /** 是否克隆数据(已废弃) */
28
- clone?: boolean;
29
- }
package/src/constants.ts DELETED
@@ -1,60 +0,0 @@
1
- /**
2
- * Befly 共享常量定义
3
- * 运行时使用的常量(非纯类型)
4
- */
5
-
6
- // ============================================
7
- // API 响应码常量
8
- // ============================================
9
-
10
- /**
11
- * API 响应码
12
- */
13
- export const ApiCode = {
14
- /** 成功 */
15
- SUCCESS: 0,
16
- /** 通用失败 */
17
- FAIL: 1,
18
- /** 未授权 */
19
- UNAUTHORIZED: 401,
20
- /** 禁止访问 */
21
- FORBIDDEN: 403,
22
- /** 未找到 */
23
- NOT_FOUND: 404,
24
- /** 服务器错误 */
25
- SERVER_ERROR: 500
26
- } as const;
27
-
28
- /**
29
- * API 响应码类型
30
- */
31
- export type ApiCodeType = (typeof ApiCode)[keyof typeof ApiCode];
32
-
33
- // ============================================
34
- // 错误消息常量
35
- // ============================================
36
-
37
- /**
38
- * 通用错误消息
39
- */
40
- export const ErrorMessages = {
41
- /** 未授权 */
42
- UNAUTHORIZED: '请先登录',
43
- /** 禁止访问 */
44
- FORBIDDEN: '无访问权限',
45
- /** 未找到 */
46
- NOT_FOUND: '资源不存在',
47
- /** 服务器错误 */
48
- SERVER_ERROR: '服务器错误',
49
- /** 参数错误 */
50
- INVALID_PARAMS: '参数错误',
51
- /** Token 过期 */
52
- TOKEN_EXPIRED: 'Token 已过期',
53
- /** Token 无效 */
54
- TOKEN_INVALID: 'Token 无效'
55
- } as const;
56
-
57
- /**
58
- * 错误消息类型
59
- */
60
- export type ErrorMessageType = (typeof ErrorMessages)[keyof typeof ErrorMessages];
@@ -1,172 +0,0 @@
1
- import { isPlainObject } from 'es-toolkit/compat';
2
- import { camelCase, snakeCase, kebabCase, pascalCase } from 'es-toolkit/string';
3
-
4
- /**
5
- * 键名转换函数类型
6
- */
7
- export type KeyTransformer = (key: string) => string;
8
-
9
- /**
10
- * 预设的转换方式
11
- * - camel: 小驼峰 user_id → userId
12
- * - snake: 下划线 userId → user_id
13
- * - kebab: 短横线 userId → user-id
14
- * - pascal: 大驼峰 user_id → UserId
15
- * - upper: 大写 userId → USERID
16
- * - lower: 小写 UserId → userid
17
- */
18
- export type PresetTransform = 'camel' | 'snake' | 'kebab' | 'pascal' | 'upper' | 'lower';
19
-
20
- /**
21
- * 转换选项
22
- */
23
- export interface TransformOptions {
24
- /** 最大递归深度,默认 100。设置为 0 表示不限制深度(不推荐) */
25
- maxDepth?: number;
26
- /** 排除的键名列表,这些键名不会被转换 */
27
- excludeKeys?: string[];
28
- }
29
-
30
- /**
31
- * 深度递归遍历数据结构,转换所有键名
32
- * 支持嵌套对象和数组,自动防止循环引用和栈溢出
33
- *
34
- * @param data - 源数据(对象、数组或其他类型)
35
- * @param transformer - 转换函数或预设方式
36
- * @param options - 转换选项
37
- * @returns 键名转换后的新数据
38
- *
39
- * @example
40
- * // 小驼峰
41
- * deepTransformKeys({ user_id: 123, user_info: { first_name: 'John' } }, 'camel')
42
- * // { userId: 123, userInfo: { firstName: 'John' } }
43
- *
44
- * @example
45
- * // 下划线
46
- * deepTransformKeys({ userId: 123, userInfo: { firstName: 'John' } }, 'snake')
47
- * // { user_id: 123, user_info: { first_name: 'John' } }
48
- *
49
- * @example
50
- * // 短横线
51
- * deepTransformKeys({ userId: 123, userInfo: { firstName: 'John' } }, 'kebab')
52
- * // { 'user-id': 123, 'user-info': { 'first-name': 'John' } }
53
- *
54
- * @example
55
- * // 大驼峰
56
- * deepTransformKeys({ user_id: 123 }, 'pascal')
57
- * // { UserId: 123 }
58
- *
59
- * @example
60
- * // 大写
61
- * deepTransformKeys({ userId: 123 }, 'upper')
62
- * // { USERID: 123 }
63
- *
64
- * @example
65
- * // 小写
66
- * deepTransformKeys({ UserId: 123 }, 'lower')
67
- * // { userid: 123 }
68
- *
69
- * @example
70
- * // 自定义转换函数
71
- * deepTransformKeys({ user_id: 123 }, (key) => `prefix_${key}`)
72
- * // { prefix_user_id: 123 }
73
- *
74
- * @example
75
- * // 限制递归深度
76
- * deepTransformKeys(deepData, 'camel', { maxDepth: 10 })
77
- *
78
- * @example
79
- * // 排除特定键名
80
- * deepTransformKeys({ _id: '123', user_name: 'John' }, 'camel', { excludeKeys: ['_id'] })
81
- * // { _id: '123', userName: 'John' }
82
- *
83
- * @example
84
- * // 嵌套数组和对象
85
- * deepTransformKeys({
86
- * user_list: [{ user_id: 1, user_tags: [{ tag_name: 'vip' }] }]
87
- * }, 'camel')
88
- * // { userList: [{ userId: 1, userTags: [{ tagName: 'vip' }] }] }
89
- */
90
- export const deepTransformKeys = <T = any>(data: any, transformer: KeyTransformer | PresetTransform, options: TransformOptions = {}): T => {
91
- const { maxDepth = 100, excludeKeys = [] } = options;
92
-
93
- // 获取实际的转换函数
94
- let transformFn: KeyTransformer;
95
- if (typeof transformer === 'function') {
96
- transformFn = transformer;
97
- } else if (transformer === 'camel') {
98
- transformFn = camelCase;
99
- } else if (transformer === 'snake') {
100
- transformFn = snakeCase;
101
- } else if (transformer === 'kebab') {
102
- transformFn = kebabCase;
103
- } else if (transformer === 'pascal') {
104
- transformFn = pascalCase;
105
- } else if (transformer === 'upper') {
106
- transformFn = (key: string) => key.toUpperCase();
107
- } else {
108
- transformFn = (key: string) => key.toLowerCase();
109
- }
110
-
111
- // 用于检测循环引用的 WeakSet
112
- const visited = new WeakSet();
113
-
114
- // 创建排除键名集合,提升查找性能
115
- const excludeSet = new Set(excludeKeys);
116
-
117
- // 递归转换函数
118
- const transform = (value: any, depth: number): any => {
119
- // 处理 null 和 undefined
120
- if (value === null || value === undefined) {
121
- return value;
122
- }
123
-
124
- // 处理数组
125
- if (Array.isArray(value)) {
126
- // 检测循环引用
127
- if (visited.has(value)) {
128
- return value;
129
- }
130
- visited.add(value);
131
-
132
- // 检查深度限制:如果达到限制,返回原数组
133
- if (maxDepth > 0 && depth >= maxDepth) {
134
- visited.delete(value);
135
- return value;
136
- }
137
-
138
- const result = value.map((item: any) => transform(item, depth + 1));
139
- visited.delete(value);
140
- return result;
141
- }
142
-
143
- // 处理对象
144
- if (isPlainObject(value)) {
145
- // 检测循环引用
146
- if (visited.has(value)) {
147
- return value;
148
- }
149
- visited.add(value);
150
-
151
- // 检查深度限制:如果达到限制,返回原对象
152
- if (maxDepth > 0 && depth >= maxDepth) {
153
- visited.delete(value);
154
- return value;
155
- }
156
-
157
- const result: any = {};
158
- for (const [key, val] of Object.entries(value)) {
159
- // 检查是否在排除列表中
160
- const transformedKey = excludeSet.has(key) ? key : transformFn(key);
161
- result[transformedKey] = transform(val, depth + 1);
162
- }
163
- visited.delete(value);
164
- return result;
165
- }
166
-
167
- // 其他类型(字符串、数字、布尔值等)直接返回
168
- return value;
169
- };
170
-
171
- return transform(data, 0) as T;
172
- };