befly-vite 1.2.9 → 1.2.11
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/README.md +5 -2
- package/{utils/layouts.js → index.browser.js} +21 -11
- package/index.js +83 -19
- package/package.json +11 -18
- package/plugins/router.js +2 -2
- package/bin/scanViewsDir.ts +0 -114
- package/configs/uno.config.js +0 -37
- package/plugins/compression.js +0 -15
- package/plugins/unocss.js +0 -8
- package/utils/arrayToTree.js +0 -100
- package/utils/createUnoConfig.js +0 -7
- package/utils/fieldClear.js +0 -94
- package/utils/hashPassword.js +0 -21
- package/utils/router.js +0 -109
- package/utils/scanViews.js +0 -59
- package/utils/withDefaultColumns.js +0 -41
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Befly Vite 配置预设和插件集合,专为 Vue 3 项目优化。
|
|
|
5
5
|
## 特性
|
|
6
6
|
|
|
7
7
|
- ✅ 开箱即用的 Vite + Vue 3 配置
|
|
8
|
-
- ✅
|
|
8
|
+
- ✅ 集成常用插件(路由、自动导入、图标等)
|
|
9
9
|
- ✅ 优化的构建配置(分包、压缩、分析)
|
|
10
10
|
- ✅ 支持自定义扩展
|
|
11
11
|
|
|
@@ -35,8 +35,11 @@ import { fileURLToPath } from "node:url";
|
|
|
35
35
|
export default createBeflyViteConfig({
|
|
36
36
|
root: fileURLToPath(new URL(".", import.meta.url)),
|
|
37
37
|
|
|
38
|
+
// 说明:项目 views 固定扫描 "src/views";如需扫描 addon 内的视图目录,可配置 addonView(单级目录名)
|
|
39
|
+
// addonView: "adminViews",
|
|
40
|
+
|
|
38
41
|
// 自定义配置
|
|
39
|
-
|
|
42
|
+
viteConfig: {
|
|
40
43
|
server: {
|
|
41
44
|
port: 5600
|
|
42
45
|
}
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* 根据文件名后缀 _数字 判断使用哪个布局
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
2
|
+
* auto-routes 的 route 结构(我们只依赖以下字段)。
|
|
7
3
|
* @typedef {Object} RouteConfig
|
|
8
4
|
* @property {string=} path
|
|
9
5
|
* @property {any=} component
|
|
@@ -12,6 +8,7 @@
|
|
|
12
8
|
*/
|
|
13
9
|
|
|
14
10
|
/**
|
|
11
|
+
* 内部扁平结构:一条“最终路由 path + 选用布局 + 页面组件”。
|
|
15
12
|
* @typedef {Object} LayoutConfig
|
|
16
13
|
* @property {string} path
|
|
17
14
|
* @property {string} layoutName
|
|
@@ -20,11 +17,15 @@
|
|
|
20
17
|
*/
|
|
21
18
|
|
|
22
19
|
/**
|
|
20
|
+
* 内部实现:根据文件名后缀 _数字 判断使用哪个布局,输出扁平布局配置。
|
|
21
|
+
*
|
|
22
|
+
* 注意:该函数仅供 befly-vite 包内部使用,不作为对外 API。
|
|
23
|
+
*
|
|
23
24
|
* @param {RouteConfig[]} routes
|
|
24
25
|
* @param {string=} inheritLayout
|
|
25
26
|
* @returns {LayoutConfig[]}
|
|
26
27
|
*/
|
|
27
|
-
|
|
28
|
+
function buildLayoutConfigs(routes, inheritLayout = "") {
|
|
28
29
|
/** @type {LayoutConfig[]} */
|
|
29
30
|
const result = [];
|
|
30
31
|
|
|
@@ -37,7 +38,7 @@ export function Layouts(routes, inheritLayout = "") {
|
|
|
37
38
|
// 中间节点:递归处理子路由,不包裹布局
|
|
38
39
|
if (route.children && route.children.length > 0) {
|
|
39
40
|
const cleanPath = pathMatch ? currentPath.replace(/_\d+$/, "") : currentPath;
|
|
40
|
-
const childConfigs =
|
|
41
|
+
const childConfigs = buildLayoutConfigs(route.children, currentLayout);
|
|
41
42
|
|
|
42
43
|
for (const child of childConfigs) {
|
|
43
44
|
const mergedPath = cleanPath ? `${cleanPath}/${child.path}`.replace(/\/+/, "/") : child.path;
|
|
@@ -77,14 +78,23 @@ export function Layouts(routes, inheritLayout = "") {
|
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
/**
|
|
80
|
-
* 将
|
|
81
|
-
* 说明:resolveLayoutComponent 由业务方提供,以避免 utils 强耦合具体项目的布局路径。
|
|
81
|
+
* 将 auto-routes 的 routes 按 `_数字` 规则套用布局组件,并输出 Vue Router 的 RouteRecordRaw[]。
|
|
82
82
|
*
|
|
83
|
-
* @param {
|
|
83
|
+
* @param {any[]} routes
|
|
84
84
|
* @param {(layoutName: string) => any} resolveLayoutComponent
|
|
85
85
|
* @returns {import('vue-router').RouteRecordRaw[]}
|
|
86
86
|
*/
|
|
87
|
-
export function
|
|
87
|
+
export function Layouts(routes, resolveLayoutComponent) {
|
|
88
|
+
if (!Array.isArray(routes)) {
|
|
89
|
+
throw new Error("Layouts(routes, resolveLayoutComponent) 中 routes 必须是数组。");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (typeof resolveLayoutComponent !== "function") {
|
|
93
|
+
throw new Error("Layouts(routes, resolveLayoutComponent) 中 resolveLayoutComponent 必须是函数。");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const configs = buildLayoutConfigs(routes);
|
|
97
|
+
|
|
88
98
|
return configs.map((config) => {
|
|
89
99
|
const layoutComponent = resolveLayoutComponent(config.layoutName);
|
|
90
100
|
|
package/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { existsSync, readdirSync, realpathSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
1
3
|
import { fileURLToPath } from "node:url";
|
|
2
4
|
|
|
3
5
|
import { defineConfig, mergeConfig } from "vite";
|
|
@@ -5,12 +7,10 @@ import { defineConfig, mergeConfig } from "vite";
|
|
|
5
7
|
import { createAnalyzerPlugin } from "./plugins/analyzer.js";
|
|
6
8
|
import { createAutoImportPlugin } from "./plugins/auto-import.js";
|
|
7
9
|
import { createComponentsPlugin } from "./plugins/components.js";
|
|
8
|
-
import { createCompressionPlugin } from "./plugins/compression.js";
|
|
9
10
|
import { createDevToolsPlugin } from "./plugins/devtools.js";
|
|
10
11
|
import { createIconsPlugin } from "./plugins/icons.js";
|
|
11
12
|
import { createReactivityTransformPlugin } from "./plugins/reactivity-transform.js";
|
|
12
13
|
import { createRouterPlugin } from "./plugins/router.js";
|
|
13
|
-
import { createUnoCSSPlugin } from "./plugins/unocss.js";
|
|
14
14
|
import { createVuePlugin } from "./plugins/vue.js";
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -66,48 +66,60 @@ function defaultManualChunks(id) {
|
|
|
66
66
|
return "vue-macros";
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// 其他 node_modules 依赖
|
|
75
|
-
if (id.includes("node_modules/")) {
|
|
76
|
-
return "vendor";
|
|
77
|
-
}
|
|
69
|
+
// 注意:不要把所有 addon 或 node_modules 强制合并到单个 chunk。
|
|
70
|
+
// - addon 路由(importMode: async)应按页面懒加载自然拆分。
|
|
71
|
+
// - node_modules 让 Rollup 按共享与动态导入边界自动拆分即可。
|
|
72
|
+
// 如需进一步细分,可通过 createBeflyViteConfig({ manualChunks }) 注入自定义策略。
|
|
78
73
|
}
|
|
79
74
|
|
|
80
75
|
/**
|
|
81
76
|
* 创建 Befly Vite 配置
|
|
82
77
|
* @param {Object} options - 配置选项
|
|
83
78
|
* @param {string} options.root - 项目根目录(可选)
|
|
84
|
-
* @param {
|
|
79
|
+
* @param {string} options.addonView - addon 内要扫描的视图目录名(可选,默认 "adminViews")
|
|
85
80
|
* @param {Object} options.resolvers - 自定义 resolvers(可选)
|
|
86
81
|
* @param {Function} options.manualChunks - 自定义分包配置(可选)
|
|
87
|
-
* @param {Object} options.
|
|
82
|
+
* @param {Object} options.viteConfig - 用户自定义配置(可选)
|
|
88
83
|
* @returns {Object} Vite 配置对象
|
|
89
84
|
*/
|
|
90
85
|
export function createBeflyViteConfig(options = {}) {
|
|
91
|
-
const { root,
|
|
86
|
+
const { root, addonView = "adminViews", resolvers = {}, manualChunks, viteConfig = {} } = options;
|
|
92
87
|
|
|
93
88
|
// 计算根目录(如果未提供)
|
|
94
89
|
const appRoot = root || process.cwd();
|
|
95
90
|
|
|
91
|
+
if (typeof addonView !== "string") {
|
|
92
|
+
throw new Error('createBeflyViteConfig({ addonView }) 中 addonView 必须是字符串目录名。\n例如:addonView: "adminViews"');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (addonView.trim() !== addonView) {
|
|
96
|
+
throw new Error('createBeflyViteConfig({ addonView }) 中 addonView 不能包含首尾空格。\n例如:addonView: "adminViews"');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!addonView) {
|
|
100
|
+
throw new Error('createBeflyViteConfig({ addonView }) 中 addonView 不能为空。\n例如:addonView: "adminViews"');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 只能是单级目录名:禁止多级路径与路径穿越
|
|
104
|
+
if (addonView === "." || addonView === ".." || addonView.includes("/") || addonView.includes("\\") || addonView.includes("..") || addonView.includes("\0")) {
|
|
105
|
+
throw new Error('createBeflyViteConfig({ addonView }) 中 addonView 必须是单级目录名(不能是多级路径)。\n例如:addonView: "adminViews"');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const routesFolders = scanViewsInternal(appRoot, addonView);
|
|
109
|
+
|
|
96
110
|
const baseConfig = defineConfig({
|
|
97
111
|
base: "./",
|
|
98
112
|
|
|
99
113
|
plugins: [
|
|
100
114
|
//
|
|
101
|
-
|
|
102
|
-
createRouterPlugin({ scanViews: scanViews }),
|
|
115
|
+
createRouterPlugin({ routesFolders: routesFolders }),
|
|
103
116
|
createVuePlugin(),
|
|
104
117
|
createReactivityTransformPlugin(),
|
|
105
118
|
createDevToolsPlugin(),
|
|
106
119
|
createAutoImportPlugin({ resolvers: resolvers }),
|
|
107
120
|
createComponentsPlugin({ resolvers: resolvers }),
|
|
108
121
|
createIconsPlugin(),
|
|
109
|
-
createAnalyzerPlugin()
|
|
110
|
-
createCompressionPlugin()
|
|
122
|
+
createAnalyzerPlugin()
|
|
111
123
|
],
|
|
112
124
|
|
|
113
125
|
resolve: {
|
|
@@ -153,5 +165,57 @@ export function createBeflyViteConfig(options = {}) {
|
|
|
153
165
|
}
|
|
154
166
|
});
|
|
155
167
|
|
|
156
|
-
return mergeConfig(baseConfig,
|
|
168
|
+
return mergeConfig(baseConfig, viteConfig);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 内部实现:扫描项目和所有 @befly-addon 包的视图目录
|
|
173
|
+
* @param {string} appRoot
|
|
174
|
+
* @param {string} addonView
|
|
175
|
+
* @returns {Array<{ src: string, path: string, exclude: string[] }>}
|
|
176
|
+
*/
|
|
177
|
+
function scanViewsInternal(appRoot, addonView = "adminViews") {
|
|
178
|
+
const addonBasePath = join(appRoot, "node_modules", "@befly-addon");
|
|
179
|
+
|
|
180
|
+
/** @type {Array<{ src: string, path: string, exclude: string[] }>} */
|
|
181
|
+
const routesFolders = [];
|
|
182
|
+
|
|
183
|
+
// 1. 项目自身 views
|
|
184
|
+
const appViewsPath = join(appRoot, "src", "views");
|
|
185
|
+
if (existsSync(appViewsPath)) {
|
|
186
|
+
routesFolders.push({
|
|
187
|
+
src: realpathSync(appViewsPath),
|
|
188
|
+
path: "",
|
|
189
|
+
exclude: ["**/components/**"]
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 2. 扫描 @befly-addon/*/<addonView>(仅此目录允许生成 addon 路由)
|
|
194
|
+
if (!existsSync(addonBasePath)) {
|
|
195
|
+
return routesFolders;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const addonDirs = readdirSync(addonBasePath);
|
|
200
|
+
|
|
201
|
+
for (const addonName of addonDirs) {
|
|
202
|
+
const addonPath = join(addonBasePath, addonName);
|
|
203
|
+
if (!existsSync(addonPath)) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const addonViewPath = join(addonPath, addonView);
|
|
208
|
+
if (existsSync(addonViewPath)) {
|
|
209
|
+
routesFolders.push({
|
|
210
|
+
src: realpathSync(addonViewPath),
|
|
211
|
+
path: `addon/${addonName}/`,
|
|
212
|
+
exclude: ["**/components/**"]
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} catch {
|
|
217
|
+
// 扫描失败保持静默,避免影响 Vite 启动
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return routesFolders;
|
|
157
221
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "befly-vite",
|
|
3
|
-
"version": "1.2.
|
|
4
|
-
"gitHead": "
|
|
3
|
+
"version": "1.2.11",
|
|
4
|
+
"gitHead": "f5d17c019a58e82cfacec5ce3a1f04e3225ba838",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Befly Vite 配置预设和插件集合",
|
|
7
7
|
"keywords": [
|
|
@@ -15,41 +15,34 @@
|
|
|
15
15
|
"license": "Apache-2.0",
|
|
16
16
|
"author": "chensuiyi <bimostyle@qq.com>",
|
|
17
17
|
"files": [
|
|
18
|
+
"index.browser.js",
|
|
18
19
|
"index.js",
|
|
19
20
|
"package.json",
|
|
20
21
|
"README.md",
|
|
21
|
-
"
|
|
22
|
-
"configs/",
|
|
23
|
-
"plugins/",
|
|
24
|
-
"utils/"
|
|
22
|
+
"plugins/"
|
|
25
23
|
],
|
|
26
24
|
"type": "module",
|
|
27
|
-
"main": "index.js",
|
|
28
25
|
"exports": {
|
|
29
|
-
".":
|
|
30
|
-
|
|
26
|
+
".": {
|
|
27
|
+
"browser": "./index.browser.js",
|
|
28
|
+
"import": "./index.js",
|
|
29
|
+
"default": "./index.js"
|
|
30
|
+
}
|
|
31
31
|
},
|
|
32
32
|
"publishConfig": {
|
|
33
33
|
"access": "public",
|
|
34
34
|
"registry": "https://registry.npmjs.org"
|
|
35
35
|
},
|
|
36
|
-
"scripts": {
|
|
37
|
-
"scanViewsDir": "bun ./bin/scanViewsDir.ts"
|
|
38
|
-
},
|
|
39
36
|
"dependencies": {
|
|
40
|
-
"@unocss/preset-attributify": "^66.5.11",
|
|
41
|
-
"@unocss/preset-uno": "^66.5.11",
|
|
42
37
|
"@vitejs/plugin-vue": "^6.0.3",
|
|
43
38
|
"@vue-macros/reactivity-transform": "^3.1.1",
|
|
44
|
-
"befly-shared": "^1.3.
|
|
39
|
+
"befly-shared": "^1.3.9",
|
|
45
40
|
"sass": "^1.97.1",
|
|
46
|
-
"unocss": "^66.5.11",
|
|
47
41
|
"unplugin-auto-import": "^20.3.0",
|
|
48
42
|
"unplugin-icons": "^22.5.0",
|
|
49
43
|
"unplugin-vue-components": "^30.0.0",
|
|
50
|
-
"unplugin-vue-router": "^0.19.
|
|
44
|
+
"unplugin-vue-router": "^0.19.2",
|
|
51
45
|
"vite-bundle-analyzer": "^1.3.1",
|
|
52
|
-
"vite-plugin-compression2": "^2.4.0",
|
|
53
46
|
"vite-plugin-vue-devtools": "^8.0.5"
|
|
54
47
|
},
|
|
55
48
|
"peerDependencies": {
|
package/plugins/router.js
CHANGED
|
@@ -4,10 +4,10 @@ import VueRouter from "unplugin-vue-router/vite";
|
|
|
4
4
|
* 创建路由插件配置
|
|
5
5
|
*/
|
|
6
6
|
export function createRouterPlugin(options = {}) {
|
|
7
|
-
const {
|
|
7
|
+
const { routesFolders } = options;
|
|
8
8
|
|
|
9
9
|
return VueRouter({
|
|
10
|
-
routesFolder:
|
|
10
|
+
routesFolder: routesFolders,
|
|
11
11
|
dts: "./src/types/typed-router.d.ts",
|
|
12
12
|
extensions: [".vue"],
|
|
13
13
|
importMode: "async",
|
package/bin/scanViewsDir.ts
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import type { ViewDirMeta } from "befly-shared/utils/scanViewsDir";
|
|
2
|
-
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
4
|
-
import { readFile, readdir, writeFile } from "node:fs/promises";
|
|
5
|
-
import { join, resolve } from "node:path";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
|
|
8
|
-
import { cleanDirName, extractDefinePageMetaFromScriptSetup, extractScriptSetupBlock, normalizeMenuTree } from "befly-shared/utils/scanViewsDir";
|
|
9
|
-
|
|
10
|
-
type MenuConfig = {
|
|
11
|
-
name: string;
|
|
12
|
-
path: string;
|
|
13
|
-
icon?: string;
|
|
14
|
-
sort?: number;
|
|
15
|
-
children?: MenuConfig[];
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* 扫描 views 目录,构建菜单树(与 core/sync/syncMenu.ts 中的 scanViewsDir 逻辑一致)
|
|
20
|
-
*/
|
|
21
|
-
export async function scanViewsDir(viewsDir: string, prefix: string, parentPath: string = ""): Promise<MenuConfig[]> {
|
|
22
|
-
if (!existsSync(viewsDir)) {
|
|
23
|
-
return [];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const menus: MenuConfig[] = [];
|
|
27
|
-
const entries = await readdir(viewsDir, { withFileTypes: true });
|
|
28
|
-
|
|
29
|
-
for (const entry of entries) {
|
|
30
|
-
if (!entry.isDirectory() || entry.name === "components") {
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const dirPath = join(viewsDir, entry.name);
|
|
35
|
-
const indexVuePath = join(dirPath, "index.vue");
|
|
36
|
-
|
|
37
|
-
if (!existsSync(indexVuePath)) {
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
let meta: ViewDirMeta | null = null;
|
|
42
|
-
try {
|
|
43
|
-
const content = await readFile(indexVuePath, "utf-8");
|
|
44
|
-
|
|
45
|
-
const scriptSetup = extractScriptSetupBlock(content);
|
|
46
|
-
if (!scriptSetup) {
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
meta = extractDefinePageMetaFromScriptSetup(scriptSetup);
|
|
51
|
-
if (!meta?.title) {
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
} catch {
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (!meta?.title) {
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const cleanName = cleanDirName(entry.name);
|
|
63
|
-
let menuPath: string;
|
|
64
|
-
if (cleanName === "index") {
|
|
65
|
-
menuPath = parentPath;
|
|
66
|
-
} else {
|
|
67
|
-
menuPath = parentPath ? `${parentPath}/${cleanName}` : `/${cleanName}`;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const fullPath = prefix ? (menuPath ? `${prefix}${menuPath}` : prefix) : menuPath || "/";
|
|
71
|
-
|
|
72
|
-
const menu: MenuConfig = {
|
|
73
|
-
name: meta.title,
|
|
74
|
-
path: fullPath,
|
|
75
|
-
sort: meta.order ?? 999999
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const children = await scanViewsDir(dirPath, prefix, menuPath);
|
|
79
|
-
if (children.length > 0) {
|
|
80
|
-
menu.children = children;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
menus.push(menu);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
menus.sort((a, b) => (a.sort ?? 999999) - (b.sort ?? 999999));
|
|
87
|
-
return menus;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async function main(): Promise<void> {
|
|
91
|
-
// 固定扫描目录:仓库 packages/admin/src/views
|
|
92
|
-
// 固定输出:同目录下的 menu.json
|
|
93
|
-
const fileDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
|
|
94
|
-
const repoRoot = resolve(fileDir, "..", "..", "..");
|
|
95
|
-
|
|
96
|
-
const absDir = resolve(repoRoot, "packages", "admin", "src", "views");
|
|
97
|
-
const outPath = join(absDir, "menu.json");
|
|
98
|
-
|
|
99
|
-
if (!existsSync(absDir)) {
|
|
100
|
-
process.stderr.write(`Missing views dir: ${absDir}\n`);
|
|
101
|
-
process.exitCode = 1;
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const menus = await scanViewsDir(absDir, "");
|
|
106
|
-
const normalized = normalizeMenuTree(menus);
|
|
107
|
-
|
|
108
|
-
const content = `${JSON.stringify(normalized, null, 4)}\n`;
|
|
109
|
-
await writeFile(outPath, content, { encoding: "utf-8" });
|
|
110
|
-
|
|
111
|
-
process.stdout.write(`Wrote ${normalized.length} root menus to ${outPath}\n`);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
await main();
|
package/configs/uno.config.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { defineConfig, presetAttributify, presetUno } from "unocss";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* 创建 UnoCSS 配置
|
|
5
|
-
*/
|
|
6
|
-
export function createUnoConfig(userConfig = {}) {
|
|
7
|
-
const defaultConfig = {
|
|
8
|
-
presets: [presetUno(), presetAttributify()],
|
|
9
|
-
shortcuts: {
|
|
10
|
-
"flex-center": "flex items-center justify-center",
|
|
11
|
-
"flex-between": "flex items-center justify-between",
|
|
12
|
-
"flex-col-center": "flex flex-col items-center justify-center"
|
|
13
|
-
},
|
|
14
|
-
theme: {
|
|
15
|
-
colors: {
|
|
16
|
-
primary: "#1890ff",
|
|
17
|
-
success: "#52c41a",
|
|
18
|
-
warning: "#faad14",
|
|
19
|
-
danger: "#ff4d4f",
|
|
20
|
-
info: "#1890ff"
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
return defineConfig({
|
|
26
|
-
...defaultConfig,
|
|
27
|
-
...userConfig,
|
|
28
|
-
theme: {
|
|
29
|
-
...defaultConfig.theme,
|
|
30
|
-
...userConfig.theme
|
|
31
|
-
},
|
|
32
|
-
shortcuts: {
|
|
33
|
-
...defaultConfig.shortcuts,
|
|
34
|
-
...userConfig.shortcuts
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
}
|
package/plugins/compression.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { compression } from "vite-plugin-compression2";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* 创建文件压缩插件配置
|
|
5
|
-
*/
|
|
6
|
-
export function createCompressionPlugin(options = {}) {
|
|
7
|
-
const { threshold = 10240, algorithms = ["gzip", "brotliCompress"] } = options;
|
|
8
|
-
|
|
9
|
-
return compression({
|
|
10
|
-
include: /\.(html|xml|css|json|js|mjs|svg)$/i,
|
|
11
|
-
threshold: threshold,
|
|
12
|
-
algorithms: algorithms,
|
|
13
|
-
deleteOriginalAssets: false
|
|
14
|
-
});
|
|
15
|
-
}
|
package/plugins/unocss.js
DELETED
package/utils/arrayToTree.js
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @typedef {Object} ArrayToTreeOptions
|
|
3
|
-
* @property {string=} idField
|
|
4
|
-
* @property {string=} pidField
|
|
5
|
-
* @property {string=} childrenField
|
|
6
|
-
* @property {any=} rootPid
|
|
7
|
-
* @property {string=} sortField
|
|
8
|
-
* @property {(node: any) => any=} mapFn
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* @template T
|
|
13
|
-
* @param {T[]} items
|
|
14
|
-
* @param {ArrayToTreeOptions=} options
|
|
15
|
-
* @returns {T[]}
|
|
16
|
-
*/
|
|
17
|
-
export function arrayToTree(items, options = {}) {
|
|
18
|
-
const idField = typeof options.idField === "string" ? options.idField : "id";
|
|
19
|
-
const pidField = typeof options.pidField === "string" ? options.pidField : "pid";
|
|
20
|
-
const childrenField = typeof options.childrenField === "string" ? options.childrenField : "children";
|
|
21
|
-
const rootPid = "rootPid" in options ? options.rootPid : 0;
|
|
22
|
-
const sortField = "sortField" in options ? (typeof options.sortField === "string" && options.sortField.length > 0 ? options.sortField : null) : "sort";
|
|
23
|
-
const mapFn = typeof options.mapFn === "function" ? options.mapFn : null;
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* pid -> items[]
|
|
27
|
-
* @type {Map<any, T[]>}
|
|
28
|
-
*/
|
|
29
|
-
const pidMap = new Map();
|
|
30
|
-
|
|
31
|
-
for (const item of items) {
|
|
32
|
-
// @ts-ignore
|
|
33
|
-
const pid = item ? item[pidField] : undefined;
|
|
34
|
-
const list = pidMap.get(pid);
|
|
35
|
-
if (list) {
|
|
36
|
-
list.push(item);
|
|
37
|
-
} else {
|
|
38
|
-
pidMap.set(pid, [item]);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* @param {any} pid
|
|
44
|
-
* @param {Set<any>} stack
|
|
45
|
-
* @returns {T[]}
|
|
46
|
-
*/
|
|
47
|
-
const build = (pid, stack) => {
|
|
48
|
-
/** @type {T[]} */
|
|
49
|
-
const tree = [];
|
|
50
|
-
const list = pidMap.get(pid) || [];
|
|
51
|
-
|
|
52
|
-
for (const item of list) {
|
|
53
|
-
const node = Object.assign({}, item);
|
|
54
|
-
const mappedNode = mapFn ? mapFn(node) : node;
|
|
55
|
-
|
|
56
|
-
// 子节点 rootPid = node[id]
|
|
57
|
-
// @ts-ignore
|
|
58
|
-
const nextRootPid = mappedNode ? mappedNode[idField] : undefined;
|
|
59
|
-
|
|
60
|
-
let children = [];
|
|
61
|
-
if (!stack.has(nextRootPid)) {
|
|
62
|
-
stack.add(nextRootPid);
|
|
63
|
-
children = build(nextRootPid, stack);
|
|
64
|
-
stack.delete(nextRootPid);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (children.length > 0) {
|
|
68
|
-
// @ts-ignore
|
|
69
|
-
mappedNode[childrenField] = children;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
tree.push(mappedNode);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (sortField) {
|
|
76
|
-
tree.sort((a, b) => {
|
|
77
|
-
// @ts-ignore
|
|
78
|
-
const av = a ? a[sortField] : undefined;
|
|
79
|
-
// @ts-ignore
|
|
80
|
-
const bv = b ? b[sortField] : undefined;
|
|
81
|
-
|
|
82
|
-
const aMissing = av === undefined || av === null;
|
|
83
|
-
const bMissing = bv === undefined || bv === null;
|
|
84
|
-
if (aMissing && bMissing) return 0;
|
|
85
|
-
if (aMissing) return 1;
|
|
86
|
-
if (bMissing) return -1;
|
|
87
|
-
|
|
88
|
-
if (typeof av === "number" && typeof bv === "number") {
|
|
89
|
-
return av - bv;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return String(av).localeCompare(String(bv), undefined, { numeric: true, sensitivity: "base" });
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return tree;
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
return build(rootPid, new Set());
|
|
100
|
-
}
|
package/utils/createUnoConfig.js
DELETED
package/utils/fieldClear.js
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @typedef {Object} FieldClearOptions
|
|
3
|
-
* @property {string[]=} pickKeys
|
|
4
|
-
* @property {string[]=} omitKeys
|
|
5
|
-
* @property {any[]=} keepValues
|
|
6
|
-
* @property {any[]=} excludeValues
|
|
7
|
-
* @property {Record<string, any>=} keepMap
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
function isObject(val) {
|
|
11
|
-
return val !== null && typeof val === "object" && !Array.isArray(val);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function isArray(val) {
|
|
15
|
-
return Array.isArray(val);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* 清理对象/数组字段
|
|
20
|
-
* - 支持 pick/omit/keepValues/excludeValues
|
|
21
|
-
* - 支持 keepMap 强制保留
|
|
22
|
-
* @template T
|
|
23
|
-
* @param {T|T[]} data
|
|
24
|
-
* @param {FieldClearOptions=} options
|
|
25
|
-
* @returns {any}
|
|
26
|
-
*/
|
|
27
|
-
export function fieldClear(data, options = {}) {
|
|
28
|
-
const pickKeys = options.pickKeys;
|
|
29
|
-
const omitKeys = options.omitKeys;
|
|
30
|
-
const keepValues = options.keepValues;
|
|
31
|
-
const excludeValues = options.excludeValues;
|
|
32
|
-
const keepMap = options.keepMap;
|
|
33
|
-
|
|
34
|
-
const filterObj = (obj) => {
|
|
35
|
-
/** @type {Record<string, any>} */
|
|
36
|
-
const result = {};
|
|
37
|
-
|
|
38
|
-
let keys = Object.keys(obj);
|
|
39
|
-
if (pickKeys && pickKeys.length) {
|
|
40
|
-
keys = keys.filter((k) => pickKeys.includes(k));
|
|
41
|
-
}
|
|
42
|
-
if (omitKeys && omitKeys.length) {
|
|
43
|
-
keys = keys.filter((k) => !omitKeys.includes(k));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
for (const key of keys) {
|
|
47
|
-
const value = obj[key];
|
|
48
|
-
|
|
49
|
-
// 1. keepMap 优先
|
|
50
|
-
if (keepMap && Object.prototype.hasOwnProperty.call(keepMap, key)) {
|
|
51
|
-
if (Object.is(keepMap[key], value)) {
|
|
52
|
-
result[key] = value;
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// 2. keepValues
|
|
58
|
-
if (keepValues && keepValues.length && !keepValues.includes(value)) {
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// 3. excludeValues
|
|
63
|
-
if (excludeValues && excludeValues.length && excludeValues.includes(value)) {
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
result[key] = value;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return result;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
if (isArray(data)) {
|
|
74
|
-
return data
|
|
75
|
-
.map((item) => {
|
|
76
|
-
if (isObject(item)) {
|
|
77
|
-
return filterObj(item);
|
|
78
|
-
}
|
|
79
|
-
return item;
|
|
80
|
-
})
|
|
81
|
-
.filter((item) => {
|
|
82
|
-
if (isObject(item)) {
|
|
83
|
-
return Object.keys(item).length > 0;
|
|
84
|
-
}
|
|
85
|
-
return true;
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (isObject(data)) {
|
|
90
|
-
return filterObj(data);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return data;
|
|
94
|
-
}
|
package/utils/hashPassword.js
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 密码哈希工具(浏览器侧)
|
|
3
|
-
* 使用 SHA-256 + 盐值对密码进行单向哈希
|
|
4
|
-
* @param {string} password
|
|
5
|
-
* @param {string=} salt
|
|
6
|
-
* @returns {Promise<string>} 十六进制哈希
|
|
7
|
-
*/
|
|
8
|
-
export async function hashPassword(password, salt = "befly") {
|
|
9
|
-
const data = String(password) + String(salt);
|
|
10
|
-
|
|
11
|
-
const encoder = new TextEncoder();
|
|
12
|
-
const dataBuffer = encoder.encode(data);
|
|
13
|
-
|
|
14
|
-
// Web Crypto API
|
|
15
|
-
const hashBuffer = await crypto.subtle.digest("SHA-256", dataBuffer);
|
|
16
|
-
|
|
17
|
-
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
18
|
-
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
19
|
-
|
|
20
|
-
return hashHex;
|
|
21
|
-
}
|
package/utils/router.js
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 路由相关工具函数(守卫 / 布局装配 / resolver 等)
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { Layouts, applyLayouts } from "./layouts.js";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* 规范化路由 path:去尾随 "/"(根路径 "/" 例外)。
|
|
9
|
-
*
|
|
10
|
-
* @param {any} path
|
|
11
|
-
* @returns {any}
|
|
12
|
-
*/
|
|
13
|
-
export function normalizeRoutePath(path) {
|
|
14
|
-
if (typeof path !== "string") {
|
|
15
|
-
return path;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const normalized = path.replace(/\/+$/, "");
|
|
19
|
-
return normalized.length === 0 ? "/" : normalized;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* 应用一个最小可用的 token 鉴权守卫(业务方提供 token 获取方式与路径)。
|
|
24
|
-
*
|
|
25
|
-
* 约定:当路由 meta.public === true 时认为是公开路由。
|
|
26
|
-
*
|
|
27
|
-
* @param {import('vue-router').Router} router
|
|
28
|
-
* @param {{
|
|
29
|
-
* getToken: () => any,
|
|
30
|
-
* loginPath: string,
|
|
31
|
-
* homePath: string
|
|
32
|
-
* }} options
|
|
33
|
-
*/
|
|
34
|
-
export function applyTokenAuthGuard(router, options) {
|
|
35
|
-
const normalizedLoginPath = normalizeRoutePath(options.loginPath);
|
|
36
|
-
const normalizedHomePath = normalizeRoutePath(options.homePath);
|
|
37
|
-
|
|
38
|
-
router.beforeEach(async (to, _from, next) => {
|
|
39
|
-
const token = options.getToken();
|
|
40
|
-
const toPath = normalizeRoutePath(to.path);
|
|
41
|
-
|
|
42
|
-
// 0. 根路径重定向
|
|
43
|
-
if (toPath === "/") {
|
|
44
|
-
return next(token ? normalizedHomePath : normalizedLoginPath);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// 1. 未登录且访问非公开路由 → 跳转登录
|
|
48
|
-
if (!token && to.meta?.public !== true && toPath !== normalizedLoginPath) {
|
|
49
|
-
return next(normalizedLoginPath);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// 2. 已登录访问登录页 → 跳转首页
|
|
53
|
-
if (token && toPath === normalizedLoginPath) {
|
|
54
|
-
return next(normalizedHomePath);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
next();
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* 将“组件/懒加载函数/Promise”统一转换为 Vue Router 可接受的懒加载 component 函数。
|
|
63
|
-
*
|
|
64
|
-
* - 如果已经是函数(通常是 `() => import(...)`),直接返回。
|
|
65
|
-
* - 否则包一层函数(使其变成 lazy component)。
|
|
66
|
-
*
|
|
67
|
-
* @param {any} value
|
|
68
|
-
* @returns {any}
|
|
69
|
-
*/
|
|
70
|
-
function toLazyComponent(value) {
|
|
71
|
-
if (typeof value === "function") {
|
|
72
|
-
return value;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return () => value;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* 创建布局组件解析器(resolver)。
|
|
80
|
-
*
|
|
81
|
-
* @param {{
|
|
82
|
-
* resolveDefaultLayout: () => any,
|
|
83
|
-
* resolveNamedLayout: (layoutName: string) => any,
|
|
84
|
-
* defaultLayoutName?: string
|
|
85
|
-
* }} options
|
|
86
|
-
* @returns {(layoutName: string) => any}
|
|
87
|
-
*/
|
|
88
|
-
export function createLayoutComponentResolver(options) {
|
|
89
|
-
const defaultLayoutName = options.defaultLayoutName || "default";
|
|
90
|
-
|
|
91
|
-
return (layoutName) => {
|
|
92
|
-
if (layoutName === defaultLayoutName) {
|
|
93
|
-
return toLazyComponent(options.resolveDefaultLayout());
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return toLazyComponent(options.resolveNamedLayout(layoutName));
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* 将 auto-routes 的 routes 按 `_数字` 规则套用布局组件,并输出 Vue Router 的 RouteRecordRaw[]。
|
|
102
|
-
*
|
|
103
|
-
* @param {any[]} routes
|
|
104
|
-
* @param {(layoutName: string) => any} resolveLayoutComponent
|
|
105
|
-
* @returns {import('vue-router').RouteRecordRaw[]}
|
|
106
|
-
*/
|
|
107
|
-
export function buildLayoutRoutes(routes, resolveLayoutComponent) {
|
|
108
|
-
return applyLayouts(Layouts(routes), resolveLayoutComponent);
|
|
109
|
-
}
|
package/utils/scanViews.js
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { existsSync, readdirSync, realpathSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* 扫描项目和所有 @befly-addon 包的视图目录
|
|
6
|
-
* 用于 unplugin-vue-router 的 routesFolder 配置
|
|
7
|
-
*
|
|
8
|
-
* 约定:addon 只允许从 adminViews 扫描路由:
|
|
9
|
-
* - <addonRoot>/adminViews
|
|
10
|
-
*
|
|
11
|
-
* 注意:此函数只能在 vite.config.js 中使用(Node.js 环境),不能在浏览器中使用
|
|
12
|
-
* @returns {Array<{ src: string, path: string, exclude: string[] }>} 路由文件夹配置数组
|
|
13
|
-
*/
|
|
14
|
-
export function scanViews() {
|
|
15
|
-
const appRoot = process.cwd();
|
|
16
|
-
const addonBasePath = join(appRoot, "node_modules", "@befly-addon");
|
|
17
|
-
|
|
18
|
-
/** @type {Array<{ src: string, path: string, exclude: string[] }>} */
|
|
19
|
-
const routesFolders = [];
|
|
20
|
-
|
|
21
|
-
// 1. 项目自身 views
|
|
22
|
-
const appViewsPath = join(appRoot, "src", "views");
|
|
23
|
-
if (existsSync(appViewsPath)) {
|
|
24
|
-
routesFolders.push({
|
|
25
|
-
src: realpathSync(appViewsPath),
|
|
26
|
-
path: "",
|
|
27
|
-
exclude: ["**/components/**"]
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// 2. 扫描 @befly-addon/*/adminViews(仅此目录允许生成 addon 路由)
|
|
32
|
-
if (!existsSync(addonBasePath)) {
|
|
33
|
-
return routesFolders;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
try {
|
|
37
|
-
const addonDirs = readdirSync(addonBasePath);
|
|
38
|
-
|
|
39
|
-
for (const addonName of addonDirs) {
|
|
40
|
-
const addonPath = join(addonBasePath, addonName);
|
|
41
|
-
if (!existsSync(addonPath)) {
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const adminViewsPath = join(addonPath, "adminViews");
|
|
46
|
-
if (existsSync(adminViewsPath)) {
|
|
47
|
-
routesFolders.push({
|
|
48
|
-
src: realpathSync(adminViewsPath),
|
|
49
|
-
path: `addon/${addonName}/`,
|
|
50
|
-
exclude: ["**/components/**"]
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
} catch {
|
|
55
|
-
// 扫描失败保持静默,避免影响 Vite 启动
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return routesFolders;
|
|
59
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 为表格列添加默认配置
|
|
3
|
-
* @param {any[]} columns
|
|
4
|
-
* @param {Record<string, any>=} customConfig
|
|
5
|
-
* @returns {any[]}
|
|
6
|
-
*/
|
|
7
|
-
export function withDefaultColumns(columns, customConfig = {}) {
|
|
8
|
-
/** @type {Record<string, any>} */
|
|
9
|
-
const specialColumnConfig = Object.assign(
|
|
10
|
-
{
|
|
11
|
-
operation: { width: 100, align: "center", fixed: "right" },
|
|
12
|
-
state: { width: 100, align: "center" },
|
|
13
|
-
sort: { width: 100, align: "center" },
|
|
14
|
-
id: { width: 200, align: "center" }
|
|
15
|
-
},
|
|
16
|
-
customConfig
|
|
17
|
-
);
|
|
18
|
-
|
|
19
|
-
return columns.map((col) => {
|
|
20
|
-
const colKey = col && col.colKey;
|
|
21
|
-
|
|
22
|
-
let specialConfig = colKey ? specialColumnConfig[colKey] : undefined;
|
|
23
|
-
|
|
24
|
-
if (!specialConfig && colKey && (colKey.endsWith("At") || colKey.endsWith("At2"))) {
|
|
25
|
-
specialConfig = { align: "center" };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const base = {
|
|
29
|
-
width: 150,
|
|
30
|
-
ellipsis: true
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const merged = Object.assign({}, base);
|
|
34
|
-
if (specialConfig) {
|
|
35
|
-
Object.assign(merged, specialConfig);
|
|
36
|
-
}
|
|
37
|
-
Object.assign(merged, col);
|
|
38
|
-
|
|
39
|
-
return merged;
|
|
40
|
-
});
|
|
41
|
-
}
|