generator-mico-cli 0.2.21 → 0.2.23
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 +33 -0
- package/bin/mico.js +15 -2
- package/generators/micro-react/index.js +44 -6
- package/generators/micro-react/meta.json +2 -1
- package/generators/micro-react/templates/.cursor/rules/always-read-docs.mdc +14 -4
- package/generators/micro-react/templates/.cursor/rules/layout-app.mdc +36 -26
- package/generators/micro-react/templates/.cursor/rules/project-overview.mdc +5 -2
- package/generators/micro-react/templates/CLAUDE.md +4 -2
- package/generators/micro-react/templates/_gitignore +3 -1
- package/generators/micro-react/templates/_npmrc +1 -0
- package/generators/micro-react/templates/apps/layout/config/config.dev.ts +7 -3
- package/generators/micro-react/templates/apps/layout/config/config.prod.development.ts +5 -0
- package/generators/micro-react/templates/apps/layout/config/config.prod.testing.ts +5 -0
- package/generators/micro-react/templates/apps/layout/config/config.prod.ts +12 -0
- package/generators/micro-react/templates/apps/layout/config/routes.ts +0 -5
- package/generators/micro-react/templates/apps/layout/docs/feature-/345/276/256/345/211/215/347/253/257/346/250/241/345/274/217.md +4 -2
- package/generators/micro-react/templates/apps/layout/docs/feature-/350/217/234/345/215/225/346/235/203/351/231/220/346/216/247/345/210/266.md +116 -56
- package/generators/micro-react/templates/apps/layout/docs/feature-/350/267/257/347/224/261/344/270/216/350/217/234/345/215/225/350/247/243/350/200/246.md +179 -0
- package/generators/micro-react/templates/apps/layout/mock/api.mock.ts +81 -61
- package/generators/micro-react/templates/apps/layout/mock/menus.ts +89 -144
- package/generators/micro-react/templates/apps/layout/mock/pages.ts +83 -0
- package/generators/micro-react/templates/apps/layout/package.json +1 -0
- package/generators/micro-react/templates/apps/layout/src/app.tsx +13 -8
- package/generators/micro-react/templates/apps/layout/src/common/menu/parser.ts +118 -43
- package/generators/micro-react/templates/apps/layout/src/common/menu/types.ts +31 -4
- package/generators/micro-react/templates/apps/layout/src/common/micro-prefetch.ts +3 -2
- package/generators/micro-react/templates/apps/layout/src/common/portal-data.ts +45 -0
- package/generators/micro-react/templates/apps/layout/src/common/request/config.ts +49 -10
- package/generators/micro-react/templates/apps/layout/src/common/request/interceptors.ts +1 -1
- package/generators/micro-react/templates/apps/layout/src/common/request/sso.ts +6 -0
- package/generators/micro-react/templates/apps/layout/src/common/theme.ts +0 -2
- package/generators/micro-react/templates/apps/layout/src/components/AppTabs/index.less +0 -1
- package/generators/micro-react/templates/apps/layout/src/components/AppTabs/index.tsx +4 -4
- package/generators/micro-react/templates/apps/layout/src/components/IconFont/index.tsx +4 -5
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.less +21 -6
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx +4 -3
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/micro-app-manager.ts +7 -1
- package/generators/micro-react/templates/apps/layout/src/components/RightContent/AvatarDropdown.tsx +12 -16
- package/generators/micro-react/templates/apps/layout/src/global.less +15 -2
- package/generators/micro-react/templates/apps/layout/src/hooks/useMenu.ts +3 -2
- package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.less +32 -4
- package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.tsx +20 -10
- package/generators/micro-react/templates/apps/layout/src/layouts/index.tsx +75 -38
- package/generators/micro-react/templates/apps/layout/src/pages/404/index.tsx +3 -7
- package/generators/micro-react/templates/apps/layout/src/services/user.ts +7 -3
- package/generators/micro-react/templates/dev.preset.json +1 -1
- package/generators/micro-react/templates/package.json +1 -0
- package/generators/micro-react/templates/scripts/apply-sentry-plugin.ts +45 -0
- package/generators/subapp-react/index.js +206 -6
- package/generators/subapp-react/templates/homepage/.env +2 -1
- package/generators/subapp-react/templates/homepage/config/config.prod.development.ts +1 -0
- package/generators/subapp-react/templates/homepage/config/config.prod.testing.ts +1 -0
- package/generators/subapp-react/templates/homepage/config/config.prod.ts +8 -0
- package/generators/subapp-react/templates/homepage/package.json +1 -0
- package/generators/subapp-react/templates/homepage/src/app.tsx +6 -0
- package/generators/subapp-umd/ignore-list.json +5 -0
- package/generators/subapp-umd/index.js +325 -0
- package/generators/subapp-umd/meta.json +11 -0
- package/generators/subapp-umd/templates/README.md +94 -0
- package/generators/subapp-umd/templates/package.json +35 -0
- package/generators/subapp-umd/templates/public/index.html +34 -0
- package/generators/subapp-umd/templates/src/App.less +15 -0
- package/generators/subapp-umd/templates/src/App.tsx +13 -0
- package/generators/subapp-umd/templates/src/index.ts +2 -0
- package/generators/subapp-umd/templates/tsconfig.json +27 -0
- package/generators/subapp-umd/templates/webpack.config.js +68 -0
- package/lib/utils.js +2 -1
- package/package.json +1 -1
|
@@ -2,9 +2,12 @@ import { layoutLogger } from '@/common/logger';
|
|
|
2
2
|
import {
|
|
3
3
|
extractRoutes,
|
|
4
4
|
filterMenuItems,
|
|
5
|
+
findPageByPath,
|
|
5
6
|
findRouteByPath,
|
|
6
|
-
|
|
7
|
+
getDynamicRoutes,
|
|
8
|
+
isSuperuserUser,
|
|
7
9
|
} from '@/common/menu';
|
|
10
|
+
import { getMenus, getPages, hasPages } from '@/common/portal-data';
|
|
8
11
|
import { getAppNameFromEntry } from '@/common/micro';
|
|
9
12
|
import AppTabs from '@/components/AppTabs';
|
|
10
13
|
import MicroAppLoader from '@/components/MicroAppLoader';
|
|
@@ -46,57 +49,89 @@ const BasicLayout: React.FC = () => {
|
|
|
46
49
|
[currentUser?.is_superuser, currentUser?.side_menus],
|
|
47
50
|
);
|
|
48
51
|
|
|
49
|
-
//
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
return extractRoutes(menus);
|
|
52
|
+
// 所有页面路由(优先 PAGES,降级 MENUS)— 用于路由匹配和渲染
|
|
53
|
+
const allPageRoutes = useMemo(() => {
|
|
54
|
+
return getDynamicRoutes();
|
|
53
55
|
}, []);
|
|
54
56
|
|
|
55
|
-
//
|
|
56
|
-
const
|
|
57
|
+
// 菜单路由(从 MENUS)— 用于权限交叉引用
|
|
58
|
+
const allMenuRoutes = useMemo(() => {
|
|
59
|
+
return extractRoutes(getMenus());
|
|
60
|
+
}, []);
|
|
61
|
+
|
|
62
|
+
const allowedMenuRoutes = useMemo(() => {
|
|
57
63
|
if (isAuthDisabled()) {
|
|
58
|
-
return
|
|
64
|
+
return allMenuRoutes;
|
|
59
65
|
}
|
|
60
|
-
const
|
|
61
|
-
const filteredMenus = filterMenuItems(menus, filterOptions);
|
|
66
|
+
const filteredMenus = filterMenuItems(getMenus(), filterOptions);
|
|
62
67
|
return extractRoutes(filteredMenus);
|
|
63
|
-
}, [filterOptions,
|
|
68
|
+
}, [filterOptions, allMenuRoutes]);
|
|
64
69
|
|
|
65
|
-
//
|
|
70
|
+
// 当前路由配置(从所有页面路由中查找)
|
|
66
71
|
const currentRoute = useMemo(() => {
|
|
67
|
-
return findRouteByPath(
|
|
68
|
-
}, [
|
|
72
|
+
return findRouteByPath(allPageRoutes, location.pathname);
|
|
73
|
+
}, [allPageRoutes, location.pathname]);
|
|
69
74
|
|
|
70
|
-
//
|
|
75
|
+
// 权限判断:菜单交叉引用 + 隐藏页面级兜底
|
|
71
76
|
const isForbidden = useMemo(() => {
|
|
72
|
-
// 关闭权限控制时,不校验权限
|
|
73
77
|
if (isAuthDisabled()) return false;
|
|
74
|
-
// 免权限校验路由,不检查菜单权限
|
|
75
78
|
if (isNoPermissionRoute(location.pathname)) return false;
|
|
76
|
-
//
|
|
77
|
-
if (currentRoute) return false;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
//
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
79
|
+
// 非动态路由,交给 Umi 处理(404 等)
|
|
80
|
+
if (!currentRoute) return false;
|
|
81
|
+
if (isSuperuserUser(currentUser?.is_superuser)) return false;
|
|
82
|
+
|
|
83
|
+
// Tier 1: 菜单权限交叉引用
|
|
84
|
+
const inAllMenu = findRouteByPath(allMenuRoutes, location.pathname);
|
|
85
|
+
if (inAllMenu) {
|
|
86
|
+
const inAllowed = findRouteByPath(allowedMenuRoutes, location.pathname);
|
|
87
|
+
const forbidden = !inAllowed;
|
|
88
|
+
|
|
89
|
+
layoutLogger.log('isForbidden (menu check):', {
|
|
90
|
+
pathname: location.pathname,
|
|
91
|
+
inAllMenu: true,
|
|
92
|
+
inAllowed: !!inAllowed,
|
|
93
|
+
forbidden,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return forbidden;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Tier 2: 隐藏页面级权限(仅在 PAGES 数据可用时生效)
|
|
100
|
+
if (!hasPages()) return false;
|
|
101
|
+
|
|
102
|
+
const page = findPageByPath(getPages(), location.pathname);
|
|
103
|
+
if (!page) return false;
|
|
104
|
+
|
|
105
|
+
if (page.adminOnly) {
|
|
106
|
+
layoutLogger.log('isForbidden (hidden page adminOnly):', {
|
|
107
|
+
pathname: location.pathname,
|
|
108
|
+
pageId: page.id,
|
|
109
|
+
});
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (page.accessControlEnabled) {
|
|
114
|
+
const sideMenus = (currentUser?.side_menus || []) as string[];
|
|
115
|
+
const forbidden = !page.routeKey || !sideMenus.includes(page.routeKey);
|
|
116
|
+
|
|
117
|
+
layoutLogger.log('isForbidden (hidden page accessControl):', {
|
|
118
|
+
pathname: location.pathname,
|
|
119
|
+
pageId: page.id,
|
|
120
|
+
routeKey: page.routeKey,
|
|
121
|
+
hasSideMenuMatch: !forbidden,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return forbidden;
|
|
125
|
+
}
|
|
92
126
|
|
|
93
|
-
return
|
|
127
|
+
return false;
|
|
94
128
|
}, [
|
|
95
129
|
currentRoute,
|
|
96
|
-
|
|
97
|
-
|
|
130
|
+
allMenuRoutes,
|
|
131
|
+
allowedMenuRoutes,
|
|
98
132
|
location.pathname,
|
|
99
|
-
|
|
133
|
+
currentUser?.is_superuser,
|
|
134
|
+
currentUser?.side_menus,
|
|
100
135
|
]);
|
|
101
136
|
|
|
102
137
|
// 判断是否需要显示布局
|
|
@@ -195,7 +230,9 @@ const BasicLayout: React.FC = () => {
|
|
|
195
230
|
<AppTabs />
|
|
196
231
|
<Content className="layout-content-outlet">
|
|
197
232
|
<Suspense
|
|
198
|
-
fallback={
|
|
233
|
+
fallback={
|
|
234
|
+
<Spin dot style={{ width: '100%', marginTop: 100 }} />
|
|
235
|
+
}
|
|
199
236
|
>
|
|
200
237
|
{renderContent()}
|
|
201
238
|
</Suspense>
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
filterMenuItems,
|
|
4
|
-
getWindowMenus,
|
|
5
|
-
type MenuFilterOptions,
|
|
6
|
-
} from '@/common/menu';
|
|
1
|
+
import { extractRoutes, filterMenuItems, type MenuFilterOptions } from '@/common/menu';
|
|
2
|
+
import { getMenus } from '@/common/portal-data';
|
|
7
3
|
import { isAuthDisabled } from '@/constants';
|
|
8
4
|
import { Button, Result, Space } from '@mico-platform/ui';
|
|
9
5
|
import { history, useModel } from '@umijs/max';
|
|
@@ -13,7 +9,7 @@ import React, { useMemo } from 'react';
|
|
|
13
9
|
* 获取第一个可访问的路由路径
|
|
14
10
|
*/
|
|
15
11
|
const getFirstAvailablePath = (filterOptions: MenuFilterOptions): string => {
|
|
16
|
-
const menus =
|
|
12
|
+
const menus = getMenus();
|
|
17
13
|
|
|
18
14
|
// 根据权限过滤菜单
|
|
19
15
|
const filteredMenus = isAuthDisabled()
|
|
@@ -10,8 +10,8 @@ const USER_INFO_API = '/api/user/info';
|
|
|
10
10
|
* - en: 2 (英文)
|
|
11
11
|
*/
|
|
12
12
|
const LOCALE_TO_LANG: Partial<Record<SupportedLocale, number>> = {
|
|
13
|
-
|
|
14
|
-
en: 2,
|
|
13
|
+
'zh-CN': 1,
|
|
14
|
+
'en-US': 2,
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
/**
|
|
@@ -42,7 +42,11 @@ export async function fetchUserInfo(): Promise<IUserInfo> {
|
|
|
42
42
|
skipProxy: true,
|
|
43
43
|
});
|
|
44
44
|
if (response.code === 200 && response.data) {
|
|
45
|
-
|
|
45
|
+
const data = response.data;
|
|
46
|
+
if (!data.user_name) {
|
|
47
|
+
data.user_name = data.username || '';
|
|
48
|
+
}
|
|
49
|
+
return data;
|
|
46
50
|
}
|
|
47
51
|
throw new Error(response.msg || '获取用户信息失败');
|
|
48
52
|
}
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@commitlint/cli": "^19.5.0",
|
|
34
34
|
"@commitlint/config-conventional": "^19.5.0",
|
|
35
|
+
"@sentry/webpack-plugin": "^4.9.1",
|
|
35
36
|
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
|
36
37
|
"@typescript-eslint/parser": "^8.54.0",
|
|
37
38
|
"dotenv-cli": "^7.4.1",
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
interface ApplySentryPluginOptions {
|
|
2
|
+
/** webpack-chain 实例(Umi chainWebpack 的参数) */
|
|
3
|
+
memo: any;
|
|
4
|
+
/** 应用名称,用于 urlPrefix 路径 */
|
|
5
|
+
appName: string;
|
|
6
|
+
/** Sentry project 名称,默认 '<%= projectName %>' */
|
|
7
|
+
project?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 往 webpack-chain 配置上挂载 Sentry sourcemap 上传插件
|
|
12
|
+
*/
|
|
13
|
+
export function applySentryPlugin({
|
|
14
|
+
memo,
|
|
15
|
+
appName,
|
|
16
|
+
project = '<%= projectName %>',
|
|
17
|
+
}: ApplySentryPluginOptions) {
|
|
18
|
+
const version = require('../package.json').version;
|
|
19
|
+
|
|
20
|
+
// 使用 Legacy Upload 方式,自托管 Sentry 不支持 Debug ID / Artifact Bundle
|
|
21
|
+
const { sentryWebpackPlugin } = require('@sentry/webpack-plugin');
|
|
22
|
+
memo.plugin('sentry').use(
|
|
23
|
+
class {
|
|
24
|
+
apply(compiler: any) {
|
|
25
|
+
sentryWebpackPlugin({
|
|
26
|
+
url: 'https://sentry.micoworld.net',
|
|
27
|
+
org: 'micocenter',
|
|
28
|
+
project,
|
|
29
|
+
/** @see https://micoworld.feishu.cn/wiki/AmXQwmnEairvQ9k2m9KcgJNcndv#share-OupCdqeeUoESO4x6YKPcrs4nn3c */
|
|
30
|
+
authToken: process.env.SENTRY_AUTH_TOKEN,
|
|
31
|
+
release: {
|
|
32
|
+
name: version,
|
|
33
|
+
uploadLegacySourcemaps: {
|
|
34
|
+
paths: ['./dist'],
|
|
35
|
+
urlPrefix: `~/portal-center/<%= projectName %>/${version}/${appName}`,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
sourcemaps: {
|
|
39
|
+
disable: true,
|
|
40
|
+
},
|
|
41
|
+
}).apply(compiler);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -118,12 +118,26 @@ module.exports = class extends Generator {
|
|
|
118
118
|
if (!input.startsWith('@')) return 'Package scope must start with @';
|
|
119
119
|
return true;
|
|
120
120
|
}
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
type: 'input',
|
|
124
|
+
name: 'devPort',
|
|
125
|
+
message: 'Dev server port',
|
|
126
|
+
default: rc.devPort || '8010',
|
|
127
|
+
validate: (input) => {
|
|
128
|
+
const port = Number(input);
|
|
129
|
+
if (!Number.isInteger(port) || port < 1024 || port > 65535) {
|
|
130
|
+
return 'Port must be an integer between 1024 and 65535';
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
121
134
|
}
|
|
122
135
|
]);
|
|
123
136
|
|
|
124
137
|
this.appName = toKebab(this.answers.appName);
|
|
125
138
|
this.appNamePascal = toPascal(this.appName);
|
|
126
139
|
this.packageScope = this.answers.packageScope;
|
|
140
|
+
this.devPort = this.answers.devPort;
|
|
127
141
|
this.templateDir = this.templatePath('homepage');
|
|
128
142
|
this.destDir = path.join(this.monorepoRoot, 'apps', this.appName);
|
|
129
143
|
} catch (error) {
|
|
@@ -172,6 +186,7 @@ module.exports = class extends Generator {
|
|
|
172
186
|
appName: this.appName,
|
|
173
187
|
AppName: this.appNamePascal,
|
|
174
188
|
packageScope: this.packageScope,
|
|
189
|
+
devPort: this.devPort,
|
|
175
190
|
micoUiVersion: `^${micoUiVer}`,
|
|
176
191
|
themeVersion: `^${themeVer}`,
|
|
177
192
|
micoUiVersionExact: micoUiVer
|
|
@@ -206,7 +221,7 @@ module.exports = class extends Generator {
|
|
|
206
221
|
|
|
207
222
|
for (const relPath of files) {
|
|
208
223
|
const destRelPath = transformDestPath(relPath);
|
|
209
|
-
const isTemplate = isTemplateFile(relPath);
|
|
224
|
+
const isTemplate = isTemplateFile(relPath) || path.basename(relPath) === '.env';
|
|
210
225
|
const tag = isTemplate ? '\x1b[32m[tpl]\x1b[0m' : '\x1b[36m[cpy]\x1b[0m';
|
|
211
226
|
this.log(` ${tag} apps/${this.appName}/${destRelPath}`);
|
|
212
227
|
|
|
@@ -243,7 +258,8 @@ module.exports = class extends Generator {
|
|
|
243
258
|
const destRelPath = transformDestPath(relPath);
|
|
244
259
|
const destPath = path.join(this.destDir, destRelPath);
|
|
245
260
|
|
|
246
|
-
|
|
261
|
+
const isEnvFile = path.basename(relPath) === '.env';
|
|
262
|
+
if (isTemplateFile(relPath) || isEnvFile) {
|
|
247
263
|
this.logger.file('template', destRelPath);
|
|
248
264
|
this.fs.copyTpl(srcPath, destPath, templateData);
|
|
249
265
|
templateCount++;
|
|
@@ -255,6 +271,10 @@ module.exports = class extends Generator {
|
|
|
255
271
|
}
|
|
256
272
|
|
|
257
273
|
this.logger.verbose(`Processed: ${templateCount} templates, ${copyCount} copied`);
|
|
274
|
+
|
|
275
|
+
this._updateDevPreset();
|
|
276
|
+
this._updateMockPages();
|
|
277
|
+
this._updateConfigDev();
|
|
258
278
|
} catch (error) {
|
|
259
279
|
console.error('');
|
|
260
280
|
console.error('❌ Error during file generation:');
|
|
@@ -264,15 +284,182 @@ module.exports = class extends Generator {
|
|
|
264
284
|
}
|
|
265
285
|
}
|
|
266
286
|
|
|
287
|
+
_updateDevPreset() {
|
|
288
|
+
const presetPath = path.join(this.monorepoRoot, 'dev.preset.json');
|
|
289
|
+
if (!fs.existsSync(presetPath)) {
|
|
290
|
+
this.logger.verbose('dev.preset.json not found, skipping preset update');
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
const preset = JSON.parse(fs.readFileSync(presetPath, 'utf-8'));
|
|
296
|
+
const fullApps = preset.presets?.full?.apps;
|
|
297
|
+
|
|
298
|
+
if (!Array.isArray(fullApps)) {
|
|
299
|
+
this.logger.verbose('dev.preset.json has no full.apps array, skipping');
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (fullApps.includes(this.appName)) {
|
|
304
|
+
this.logger.verbose(`"${this.appName}" already in full preset, skipping`);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
fullApps.push(this.appName);
|
|
309
|
+
fs.writeFileSync(presetPath, `${JSON.stringify(preset, null, 2)}\n`, 'utf-8');
|
|
310
|
+
|
|
311
|
+
this.log(` 📝 已将 "${this.appName}" 添加到 dev.preset.json 的 full 预设中`);
|
|
312
|
+
} catch (e) {
|
|
313
|
+
console.warn(` ⚠️ 更新 dev.preset.json 失败: ${e.message}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
_updateMockPages() {
|
|
318
|
+
const pagesPath = path.join(this.monorepoRoot, 'apps/layout/mock/pages.ts');
|
|
319
|
+
if (!fs.existsSync(pagesPath)) {
|
|
320
|
+
this.logger.verbose('apps/layout/mock/pages.ts not found, skipping');
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
const content = fs.readFileSync(pagesPath, 'utf-8');
|
|
326
|
+
|
|
327
|
+
if (content.includes(`route: '/${this.appName}'`)) {
|
|
328
|
+
this.logger.verbose(`Route "/${this.appName}" already in mock pages, skipping`);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// 找出已有的最大 id
|
|
333
|
+
const idMatches = [...content.matchAll(/id:\s*(\d+)/g)];
|
|
334
|
+
const maxId = idMatches.reduce((max, m) => Math.max(max, Number(m[1])), 0);
|
|
335
|
+
|
|
336
|
+
const newEntry = [
|
|
337
|
+
' {',
|
|
338
|
+
` id: ${maxId + 1},`,
|
|
339
|
+
` name: '${this.appNamePascal}',`,
|
|
340
|
+
` nameEn: '${this.appNamePascal}',`,
|
|
341
|
+
` nameKey: 'page.${this.appName}',`,
|
|
342
|
+
` route: '/${this.appName}',`,
|
|
343
|
+
` htmlUrl: '//localhost:${this.devPort}',`,
|
|
344
|
+
' jsUrls: [],',
|
|
345
|
+
' cssUrls: [],',
|
|
346
|
+
" prefixPath: '',",
|
|
347
|
+
" routeMode: 'prefix',",
|
|
348
|
+
' enabled: true,',
|
|
349
|
+
' accessControlEnabled: false,',
|
|
350
|
+
' adminOnly: false,',
|
|
351
|
+
' routeKey: null,',
|
|
352
|
+
` mainDocumentId: ${Math.floor(Math.random() * 900) + 100},`,
|
|
353
|
+
" version: '',",
|
|
354
|
+
' },',
|
|
355
|
+
].join('\n');
|
|
356
|
+
|
|
357
|
+
// 插入到兜底路由 (route: '/*') 之前
|
|
358
|
+
const lines = content.split('\n');
|
|
359
|
+
let insertIdx = -1;
|
|
360
|
+
|
|
361
|
+
for (let i = 0; i < lines.length; i++) {
|
|
362
|
+
if (lines[i].includes("route: '/*'")) {
|
|
363
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
364
|
+
if (lines[j].trim() === '{') {
|
|
365
|
+
insertIdx = j;
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (insertIdx === -1) {
|
|
374
|
+
const closingIdx = lines.findIndex((l) => l.trim() === '];');
|
|
375
|
+
if (closingIdx > 0) insertIdx = closingIdx;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (insertIdx > 0) {
|
|
379
|
+
lines.splice(insertIdx, 0, newEntry);
|
|
380
|
+
fs.writeFileSync(pagesPath, lines.join('\n'), 'utf-8');
|
|
381
|
+
this.log(` 📝 已将 "${this.appName}" 的页面配置添加到 mock/pages.ts`);
|
|
382
|
+
}
|
|
383
|
+
} catch (e) {
|
|
384
|
+
console.warn(` ⚠️ 更新 mock/pages.ts 失败: ${e.message}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
_updateConfigDev() {
|
|
389
|
+
const configPath = path.join(this.monorepoRoot, 'apps/layout/config/config.dev.ts');
|
|
390
|
+
if (!fs.existsSync(configPath)) {
|
|
391
|
+
this.logger.verbose('apps/layout/config/config.dev.ts not found, skipping');
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
397
|
+
const routeEntry = `'/${this.appName}/*'`;
|
|
398
|
+
|
|
399
|
+
if (content.includes(routeEntry)) {
|
|
400
|
+
this.logger.verbose(`Route "${routeEntry}" already in noPermissionRouteList, skipping`);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const updated = content.replace(
|
|
405
|
+
/noPermissionRouteList:\s*\[([^\]]*)\]/,
|
|
406
|
+
(match, inner) => {
|
|
407
|
+
const trimmed = inner.trim();
|
|
408
|
+
const entries = trimmed ? `${trimmed}, ${routeEntry}` : routeEntry;
|
|
409
|
+
return `noPermissionRouteList: [${entries}]`;
|
|
410
|
+
},
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
if (updated === content) {
|
|
414
|
+
this.logger.verbose('noPermissionRouteList not found in config.dev.ts, skipping');
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
fs.writeFileSync(configPath, updated, 'utf-8');
|
|
419
|
+
this.log(` 📝 已将 "${routeEntry}" 添加到 config.dev.ts 的 noPermissionRouteList 中`);
|
|
420
|
+
} catch (e) {
|
|
421
|
+
console.warn(` ⚠️ 更新 config.dev.ts 失败: ${e.message}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
267
425
|
install() {
|
|
268
426
|
// 跳过 dry-run 模式
|
|
269
427
|
if (this._skipInstall) return;
|
|
270
428
|
|
|
271
429
|
this.log('');
|
|
272
430
|
this.log('📦 正在安装依赖...');
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
431
|
+
try {
|
|
432
|
+
this.spawnCommandSync('pnpm', ['install'], {
|
|
433
|
+
cwd: this.monorepoRoot
|
|
434
|
+
});
|
|
435
|
+
} catch (e) {
|
|
436
|
+
this._installFailed = true;
|
|
437
|
+
this.log('');
|
|
438
|
+
this.log('⚠️ 依赖安装失败,但子应用文件已全部生成。');
|
|
439
|
+
this.logger.verbose('Install error:', e.message);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// 依赖安装成功后,格式化被修改的 layout 文件
|
|
444
|
+
const layoutDir = path.join(this.monorepoRoot, 'apps/layout');
|
|
445
|
+
if (fs.existsSync(path.join(layoutDir, '.prettierrc'))) {
|
|
446
|
+
const filesToFormat = [
|
|
447
|
+
path.join(layoutDir, 'mock/pages.ts'),
|
|
448
|
+
path.join(layoutDir, 'config/config.dev.ts'),
|
|
449
|
+
].filter((f) => fs.existsSync(f));
|
|
450
|
+
|
|
451
|
+
if (filesToFormat.length > 0) {
|
|
452
|
+
try {
|
|
453
|
+
this.spawnCommandSync(
|
|
454
|
+
'pnpm',
|
|
455
|
+
['-C', 'apps/layout', 'exec', 'prettier', '--write', ...filesToFormat],
|
|
456
|
+
{ cwd: this.monorepoRoot },
|
|
457
|
+
);
|
|
458
|
+
} catch (e) {
|
|
459
|
+
this.logger.verbose('Formatting failed (non-critical):', e.message);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
276
463
|
}
|
|
277
464
|
|
|
278
465
|
end() {
|
|
@@ -280,7 +467,20 @@ module.exports = class extends Generator {
|
|
|
280
467
|
if (this._skipInstall) return;
|
|
281
468
|
|
|
282
469
|
this.log('');
|
|
283
|
-
|
|
470
|
+
|
|
471
|
+
if (this._installFailed) {
|
|
472
|
+
this.log('⚠️ 子应用文件已创建,但依赖安装未完成。');
|
|
473
|
+
this.log('');
|
|
474
|
+
this.log(' 请手动安装依赖:');
|
|
475
|
+
this.log('');
|
|
476
|
+
this.log(' pnpm install');
|
|
477
|
+
this.log('');
|
|
478
|
+
this.log(' 如果安装仍然失败,可尝试:');
|
|
479
|
+
this.log(' pnpm install --network-concurrency 4');
|
|
480
|
+
} else {
|
|
481
|
+
this.log('✅ 子应用创建成功!');
|
|
482
|
+
}
|
|
483
|
+
|
|
284
484
|
this.log('');
|
|
285
485
|
this.log(' 后续步骤:');
|
|
286
486
|
this.log('');
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// https://umijs.org/config/
|
|
2
2
|
|
|
3
3
|
import { defineConfig } from '@umijs/max';
|
|
4
|
+
import { applySentryPlugin } from '../../../scripts/apply-sentry-plugin';
|
|
4
5
|
const { CDN_PUBLIC_PATH } = process.env;
|
|
5
6
|
|
|
6
7
|
const PUBLIC_PATH: string = CDN_PUBLIC_PATH
|
|
@@ -8,6 +9,11 @@ const PUBLIC_PATH: string = CDN_PUBLIC_PATH
|
|
|
8
9
|
: '/<%= appName %>/';
|
|
9
10
|
|
|
10
11
|
const config: ReturnType<typeof defineConfig> = {
|
|
12
|
+
/**
|
|
13
|
+
* 启用 Source Map(用于 Sentry 错误追踪)
|
|
14
|
+
* hidden-source-map 会生成 .map 文件但不在 JS 中添加 sourcemap 注释
|
|
15
|
+
*/
|
|
16
|
+
devtool: 'hidden-source-map',
|
|
11
17
|
// 生产环境:将所有代码打包到一个文件
|
|
12
18
|
extraBabelPlugins: ['babel-plugin-dynamic-import-node'],
|
|
13
19
|
|
|
@@ -17,6 +23,7 @@ const config: ReturnType<typeof defineConfig> = {
|
|
|
17
23
|
memo.optimization.splitChunks(false);
|
|
18
24
|
// 禁用 runtimeChunk
|
|
19
25
|
memo.optimization.runtimeChunk(false);
|
|
26
|
+
applySentryPlugin({ memo, appName: '<%= appName %>' });
|
|
20
27
|
return memo;
|
|
21
28
|
},
|
|
22
29
|
publicPath: PUBLIC_PATH,
|
|
@@ -34,6 +41,7 @@ const config: ReturnType<typeof defineConfig> = {
|
|
|
34
41
|
react: 'React',
|
|
35
42
|
'react-dom': 'ReactDOM',
|
|
36
43
|
'@mico-platform/ui': 'micoUI',
|
|
44
|
+
'@common-web/sentry': 'CommonWebSentry',
|
|
37
45
|
},
|
|
38
46
|
};
|
|
39
47
|
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* 自定义函数请放在 @/common/mainApp.ts 中
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import React from 'react';
|
|
10
11
|
import { history } from '@umijs/max';
|
|
12
|
+
import { SentryErrorBoundary } from '@common-web/sentry';
|
|
11
13
|
import { appLogger } from './common/logger';
|
|
12
14
|
import { type IMicroAppProps, setMainAppProps } from './common/mainApp';
|
|
13
15
|
|
|
@@ -95,3 +97,7 @@ export async function getInitialState() {
|
|
|
95
97
|
name: '<%= appName %>',
|
|
96
98
|
};
|
|
97
99
|
}
|
|
100
|
+
|
|
101
|
+
export function rootContainer(container: React.ReactNode) {
|
|
102
|
+
return <SentryErrorBoundary>{container}</SentryErrorBoundary>;
|
|
103
|
+
}
|