generator-mico-cli 0.2.23 → 0.2.25
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 +104 -14
- package/bin/mico.js +16 -4
- package/generators/micro-react/templates/CICD/start_dev.sh +12 -2
- package/generators/micro-react/templates/CICD/start_prod.sh +11 -1
- package/generators/micro-react/templates/CICD/start_test.sh +12 -1
- package/generators/micro-react/templates/apps/layout/config/config.dev.ts +1 -1
- package/generators/micro-react/templates/apps/layout/config/config.prod.development.ts +11 -1
- package/generators/micro-react/templates/apps/layout/config/config.prod.testing.ts +11 -1
- package/generators/micro-react/templates/apps/layout/config/config.prod.ts +0 -1
- package/generators/micro-react/templates/apps/layout/docs/fix-SSO/346/227/240/351/231/220/351/207/215/345/256/232/345/220/221.md +88 -0
- package/generators/micro-react/templates/apps/layout/package.json +0 -1
- package/generators/micro-react/templates/apps/layout/src/app.tsx +2 -0
- package/generators/micro-react/templates/apps/layout/src/common/menu/parser.ts +7 -2
- package/generators/micro-react/templates/apps/layout/src/common/request/config.ts +3 -3
- package/generators/micro-react/templates/apps/layout/src/common/request/interceptors.ts +29 -1
- package/generators/micro-react/templates/apps/layout/src/common/request/sso.ts +16 -10
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx +7 -3
- package/generators/micro-react/templates/apps/layout/src/layouts/index.less +1 -1
- package/generators/micro-react/templates/package.json +1 -0
- package/generators/micro-react/templates/scripts/apply-sentry-plugin.ts +1 -1
- package/generators/micro-react/templates/scripts/collect-dist.js +10 -0
- package/generators/micro-react/templates/turbo.json +1 -1
- package/generators/subapp-react/index.js +3 -20
- package/generators/subapp-react/templates/homepage/config/config.prod.development.ts +7 -0
- package/generators/subapp-react/templates/homepage/config/config.prod.testing.ts +7 -0
- package/generators/subapp-react/templates/homepage/package.json +1 -1
- package/generators/subapp-umd/index.js +2 -18
- package/generators/subapp-umd/templates/webpack.config.js +2 -0
- package/lib/utils.js +54 -13
- package/package.json +17 -4
package/README.md
CHANGED
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
| [docs/micro-react-generator.md](docs/micro-react-generator.md) | Monorepo 项目生成器说明 |
|
|
11
11
|
| [docs/subapp-react-generator.md](docs/subapp-react-generator.md) | React 子应用生成器说明 |
|
|
12
12
|
| [docs/subapp-umd-generator.md](docs/subapp-umd-generator.md) | UMD 组件包生成器说明 |
|
|
13
|
+
| [docs/feat-integration-tests.md](docs/feat-integration-tests.md) | 集成测试方案(Vitest + yeoman-test) |
|
|
14
|
+
| [docs/fix-command-injection.md](docs/fix-command-injection.md) | 修复 npm version 查询命令注入风险 |
|
|
15
|
+
| [docs/refactor-code-quality-optimizations.md](docs/refactor-code-quality-optimizations.md) | 代码质量优化(DRY / 错误处理 / 确定性 ID / 可配置化 / 冗余清理) |
|
|
13
16
|
|
|
14
17
|
## 要求
|
|
15
18
|
|
|
@@ -69,20 +72,39 @@ mico create micro-react -- --help
|
|
|
69
72
|
|
|
70
73
|
## 配置文件
|
|
71
74
|
|
|
72
|
-
可以在项目目录或用户主目录创建 `.micorc` 或 `.micorc.json`
|
|
75
|
+
可以在项目目录或用户主目录创建 `.micorc` 或 `.micorc.json` 文件来预设默认值。
|
|
76
|
+
|
|
77
|
+
配置查找顺序:
|
|
78
|
+
1. 当前目录(或 monorepo 根目录)的 `.micorc` / `.micorc.json`
|
|
79
|
+
2. 用户主目录的 `.micorc` / `.micorc.json`
|
|
80
|
+
|
|
81
|
+
### 完整字段列表
|
|
73
82
|
|
|
74
83
|
```json
|
|
75
84
|
{
|
|
85
|
+
"projectName": "my-app",
|
|
76
86
|
"packageScope": "@my-company",
|
|
77
87
|
"cdnPrefix": "portal",
|
|
78
88
|
"author": "Team <team@example.com>",
|
|
79
|
-
"defaultSubappName": "subapp"
|
|
89
|
+
"defaultSubappName": "subapp",
|
|
90
|
+
"devPort": "8010",
|
|
91
|
+
"defaultUmdName": "my-widget",
|
|
92
|
+
"umdDevPort": "9100"
|
|
80
93
|
}
|
|
81
94
|
```
|
|
82
95
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
96
|
+
| 字段 | 类型 | 适用生成器 | 说明 | 默认值 |
|
|
97
|
+
|------|------|-----------|------|--------|
|
|
98
|
+
| `projectName` | string | micro-react | 项目名称 | 当前目录名 |
|
|
99
|
+
| `packageScope` | string | 所有 | 包作用域(如 `@my-company`) | micro-react: `@<projectName>`;其他: 从根 `package.json` 检测 |
|
|
100
|
+
| `cdnPrefix` | string | micro-react | CDN 路径前缀(如 `portal`、`admin/v2`) | 空字符串 |
|
|
101
|
+
| `author` | string | micro-react | 作者信息 | `Your Name <email@example.com>` |
|
|
102
|
+
| `defaultSubappName` | string | subapp-react | 子应用默认名称 | `subapp` |
|
|
103
|
+
| `devPort` | string | subapp-react | 子应用开发端口 | `8010` |
|
|
104
|
+
| `defaultUmdName` | string | subapp-umd | UMD 包默认名称 | `my-widget` |
|
|
105
|
+
| `umdDevPort` | string | subapp-umd | UMD 包开发端口 | `9100` |
|
|
106
|
+
|
|
107
|
+
所有字段均为可选,未配置时使用默认值或交互式提示。
|
|
86
108
|
|
|
87
109
|
## Monorepo 项目生成器 (micro-react)
|
|
88
110
|
|
|
@@ -138,9 +160,9 @@ mico create subapp-umd
|
|
|
138
160
|
|
|
139
161
|
DevTools Sources 面板中可在 `webpack://<name>/src/` 下定位原始 TypeScript 源码进行断点调试。
|
|
140
162
|
|
|
141
|
-
##
|
|
163
|
+
## 开发指南
|
|
142
164
|
|
|
143
|
-
|
|
165
|
+
### 环境准备
|
|
144
166
|
|
|
145
167
|
```bash
|
|
146
168
|
# 1. 安装依赖
|
|
@@ -168,24 +190,92 @@ mico create subapp-react
|
|
|
168
190
|
mico create subapp-umd
|
|
169
191
|
```
|
|
170
192
|
|
|
171
|
-
|
|
193
|
+
调试完成后,取消全局链接:
|
|
172
194
|
|
|
173
195
|
```bash
|
|
174
196
|
npm unlink -g generator-mico-cli
|
|
175
197
|
```
|
|
176
198
|
|
|
177
|
-
|
|
199
|
+
### 运行测试
|
|
178
200
|
|
|
179
|
-
|
|
201
|
+
项目使用 Vitest + yeoman-test 进行单元测试和集成测试:
|
|
180
202
|
|
|
181
203
|
```bash
|
|
182
|
-
|
|
204
|
+
# 运行全部测试
|
|
205
|
+
pnpm test
|
|
206
|
+
|
|
207
|
+
# watch 模式
|
|
208
|
+
pnpm test:watch
|
|
183
209
|
```
|
|
184
210
|
|
|
185
|
-
|
|
211
|
+
测试目录结构:
|
|
212
|
+
|
|
213
|
+
| 目录/文件 | 说明 |
|
|
214
|
+
|----------|------|
|
|
215
|
+
| `tests/unit/utils.test.js` | `lib/utils.js` 的单元测试 |
|
|
216
|
+
| `tests/integration/micro-react.test.js` | micro-react 生成器集成测试 |
|
|
217
|
+
| `tests/integration/subapp-react.test.js` | subapp-react 生成器集成测试 |
|
|
218
|
+
| `tests/integration/subapp-umd.test.js` | subapp-umd 生成器集成测试 |
|
|
219
|
+
| `tests/helpers/setup.js` | 测试共享工具(路径常量、monorepo fixture 工厂) |
|
|
220
|
+
|
|
221
|
+
### Scripts 说明
|
|
222
|
+
|
|
223
|
+
| 脚本 | 命令 | 说明 |
|
|
224
|
+
|------|------|------|
|
|
225
|
+
| `sync:subapp-react-template` | `pnpm run sync:subapp-react-template -- <source-path>` | 将源项目的 `apps/homepage` 同步到 `subapp-react` 模板目录,忽略清单见 `generators/subapp-react/ignore-list.json` |
|
|
226
|
+
| `verify:micro-react` | `node scripts/verify-micro-react.js [test-dir]` | 端到端验证 micro-react + subapp-react 生成器,自动创建项目并回车通过所有提示 |
|
|
227
|
+
|
|
228
|
+
#### verify-micro-react.js
|
|
229
|
+
|
|
230
|
+
自动化验证脚本,依次执行:
|
|
231
|
+
1. 创建或清空测试目录
|
|
232
|
+
2. 运行 `mico create micro-react`(自动回车通过 5 个提示)
|
|
233
|
+
3. 运行 `mico create subapp-react`(自动回车通过 4 个提示)
|
|
234
|
+
|
|
235
|
+
测试目录支持三种方式指定:
|
|
236
|
+
- 命令行参数:`node scripts/verify-micro-react.js /path/to/dir`
|
|
237
|
+
- 环境变量:`VERIFY_TEST_DIR=/path node scripts/verify-micro-react.js`
|
|
238
|
+
- 默认值:`../test-app2`
|
|
239
|
+
|
|
240
|
+
#### sync-subapp-react-template.js
|
|
241
|
+
|
|
242
|
+
从源 monorepo 项目同步 `apps/homepage` 到生成器模板目录:
|
|
186
243
|
|
|
187
244
|
```bash
|
|
188
|
-
|
|
245
|
+
# 通过环境变量
|
|
246
|
+
SOURCE_PROJECT_ROOT=<source-path> pnpm run sync:subapp-react-template
|
|
247
|
+
|
|
248
|
+
# 通过命令行参数
|
|
249
|
+
pnpm run sync:subapp-react-template -- <source-path>
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
会自动记录源仓库的 git commit 信息到 `.template-version.json`。
|
|
253
|
+
|
|
254
|
+
### 项目结构
|
|
255
|
+
|
|
256
|
+
```
|
|
257
|
+
mico-cli/
|
|
258
|
+
├── bin/mico.js # CLI 入口(命令解析、生成器调度)
|
|
259
|
+
├── lib/utils.js # 共享工具函数(字符串转换、文件处理、npm 版本查询、配置加载)
|
|
260
|
+
├── generators/
|
|
261
|
+
│ ├── micro-react/ # Monorepo 项目生成器
|
|
262
|
+
│ ├── subapp-react/ # React 子应用生成器
|
|
263
|
+
│ └── subapp-umd/ # UMD 组件包生成器
|
|
264
|
+
├── scripts/ # 辅助脚本
|
|
265
|
+
├── tests/ # 测试文件
|
|
266
|
+
├── docs/ # 功能文档
|
|
267
|
+
└── vitest.config.js # 测试配置
|
|
189
268
|
```
|
|
190
269
|
|
|
191
|
-
|
|
270
|
+
### 提交规范
|
|
271
|
+
|
|
272
|
+
请使用语义化提交信息:
|
|
273
|
+
|
|
274
|
+
```
|
|
275
|
+
feat: 添加新功能
|
|
276
|
+
fix: 修复 Bug
|
|
277
|
+
refactor: 代码重构
|
|
278
|
+
docs: 文档更新
|
|
279
|
+
test: 测试相关
|
|
280
|
+
chore: 构建/工具链变更
|
|
281
|
+
```
|
package/bin/mico.js
CHANGED
|
@@ -6,6 +6,10 @@ const fs = require('node:fs');
|
|
|
6
6
|
const path = require('node:path');
|
|
7
7
|
const readline = require('node:readline');
|
|
8
8
|
|
|
9
|
+
const { setupErrorHandlers } = require('../lib/utils');
|
|
10
|
+
|
|
11
|
+
setupErrorHandlers();
|
|
12
|
+
|
|
9
13
|
const rootDir = path.resolve(__dirname, '..');
|
|
10
14
|
const pkg = JSON.parse(
|
|
11
15
|
fs.readFileSync(path.join(rootDir, 'package.json'), 'utf8')
|
|
@@ -85,8 +89,8 @@ function listGenerators() {
|
|
|
85
89
|
if (fs.existsSync(metaPath)) {
|
|
86
90
|
try {
|
|
87
91
|
meta = { ...meta, ...JSON.parse(fs.readFileSync(metaPath, 'utf8')) };
|
|
88
|
-
} catch {
|
|
89
|
-
|
|
92
|
+
} catch (e) {
|
|
93
|
+
console.warn(` ⚠ Failed to parse ${metaPath}: ${e.message}`);
|
|
90
94
|
}
|
|
91
95
|
}
|
|
92
96
|
|
|
@@ -409,17 +413,25 @@ function runGenerator(generator, rest, passthroughArgs, options = {}) {
|
|
|
409
413
|
|
|
410
414
|
const child = spawn('yo', yoArgs, { stdio: 'inherit', env });
|
|
411
415
|
|
|
416
|
+
let exited = false;
|
|
417
|
+
const quit = (code) => {
|
|
418
|
+
if (!exited) {
|
|
419
|
+
exited = true;
|
|
420
|
+
process.exit(code);
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
|
|
412
424
|
child.on('error', (error) => {
|
|
413
425
|
if (error && error.code === 'ENOENT') {
|
|
414
426
|
console.error('Cannot find "yo". Install it with: npm install -g yo');
|
|
415
427
|
} else {
|
|
416
428
|
console.error(error);
|
|
417
429
|
}
|
|
418
|
-
|
|
430
|
+
quit(1);
|
|
419
431
|
});
|
|
420
432
|
|
|
421
433
|
child.on('exit', (code) => {
|
|
422
|
-
|
|
434
|
+
quit(typeof code === 'number' ? code : 1);
|
|
423
435
|
});
|
|
424
436
|
}
|
|
425
437
|
|
|
@@ -34,11 +34,21 @@ fi
|
|
|
34
34
|
export CDN_PUBLIC_PATH="https://cdn-portal-dev.micoplatform.com/<%= cdnPrefixPath %><%= projectName %>/${VERSION}/"
|
|
35
35
|
echo "CDN_PUBLIC_PATH: $CDN_PUBLIC_PATH"
|
|
36
36
|
|
|
37
|
+
# 开发环境,不上传 sourcemap。调试时如有需要再解开
|
|
38
|
+
# # ========== Sentry Auth Token ==========
|
|
39
|
+
# # SENTRY_AUTH_TOKEN 从 Jenkins 服务器文件读取(敏感信息,不写在脚本中)
|
|
40
|
+
# SENTRY_TOKEN_FILE="/var/lib/jenkins/.portal_sentry_auth_token"
|
|
41
|
+
# if [ -f "$SENTRY_TOKEN_FILE" ]; then
|
|
42
|
+
# export SENTRY_AUTH_TOKEN=$(cat "$SENTRY_TOKEN_FILE")
|
|
43
|
+
# fi
|
|
44
|
+
|
|
45
|
+
# if [ -z "$SENTRY_AUTH_TOKEN" ]; then
|
|
46
|
+
# echo "警告:SENTRY_AUTH_TOKEN 未设置(文件 $SENTRY_TOKEN_FILE 不存在),跳过 sourcemap 上传"
|
|
47
|
+
# fi
|
|
48
|
+
|
|
37
49
|
pnpm run build:development
|
|
38
50
|
|
|
39
51
|
# 只有在 CI 环境时才写入版本号文件(覆盖写入)
|
|
40
52
|
if [ "${CI}" = "true" ]; then
|
|
41
53
|
echo "VERSION=$VERSION" > .env_x_<%= projectName %>
|
|
42
54
|
fi
|
|
43
|
-
|
|
44
|
-
|
|
@@ -34,10 +34,20 @@ fi
|
|
|
34
34
|
export CDN_PUBLIC_PATH="https://cdn-portal.micoplatform.com/<%= cdnPrefixPath %><%= projectName %>/${VERSION}/"
|
|
35
35
|
echo "CDN_PUBLIC_PATH: $CDN_PUBLIC_PATH"
|
|
36
36
|
|
|
37
|
+
# ========== Sentry Auth Token ==========
|
|
38
|
+
# SENTRY_AUTH_TOKEN 从 Jenkins 服务器文件读取(敏感信息,不写在脚本中)
|
|
39
|
+
SENTRY_TOKEN_FILE="/var/lib/jenkins/.portal_sentry_auth_token"
|
|
40
|
+
if [ -f "$SENTRY_TOKEN_FILE" ]; then
|
|
41
|
+
export SENTRY_AUTH_TOKEN=$(cat "$SENTRY_TOKEN_FILE")
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
if [ -z "$SENTRY_AUTH_TOKEN" ]; then
|
|
45
|
+
echo "警告:SENTRY_AUTH_TOKEN 未设置(文件 $SENTRY_TOKEN_FILE 不存在),跳过 sourcemap 上传"
|
|
46
|
+
fi
|
|
47
|
+
|
|
37
48
|
pnpm run build:production
|
|
38
49
|
|
|
39
50
|
# 只有在 CI 环境时才写入版本号文件(覆盖写入)
|
|
40
51
|
if [ "${CI}" = "true" ]; then
|
|
41
52
|
echo "VERSION=$VERSION" > .env_x_<%= projectName %>
|
|
42
53
|
fi
|
|
43
|
-
|
|
@@ -35,10 +35,21 @@ fi
|
|
|
35
35
|
export CDN_PUBLIC_PATH="https://cdn-portal-test.micoplatform.com/<%= cdnPrefixPath %><%= projectName %>/${VERSION}/"
|
|
36
36
|
echo "CDN_PUBLIC_PATH: $CDN_PUBLIC_PATH"
|
|
37
37
|
|
|
38
|
+
# 测试环境,不上传 sourcemap。调试时如有需要再解开
|
|
39
|
+
# # ========== Sentry Auth Token ==========
|
|
40
|
+
# # SENTRY_AUTH_TOKEN 从 Jenkins 服务器文件读取(敏感信息,不写在脚本中)
|
|
41
|
+
# SENTRY_TOKEN_FILE="/var/lib/jenkins/.portal_sentry_auth_token"
|
|
42
|
+
# if [ -f "$SENTRY_TOKEN_FILE" ]; then
|
|
43
|
+
# export SENTRY_AUTH_TOKEN=$(cat "$SENTRY_TOKEN_FILE")
|
|
44
|
+
# fi
|
|
45
|
+
|
|
46
|
+
# if [ -z "$SENTRY_AUTH_TOKEN" ]; then
|
|
47
|
+
# echo "警告:SENTRY_AUTH_TOKEN 未设置(文件 $SENTRY_TOKEN_FILE 不存在),跳过 sourcemap 上传"
|
|
48
|
+
# fi
|
|
49
|
+
|
|
38
50
|
pnpm run build:testing
|
|
39
51
|
|
|
40
52
|
# 只有在 CI 环境时才写入版本号文件(覆盖写入)
|
|
41
53
|
if [ "${CI}" = "true" ]; then
|
|
42
54
|
echo "VERSION=$VERSION" > .env_x_<%= projectName %>
|
|
43
55
|
fi
|
|
44
|
-
|
|
@@ -19,6 +19,7 @@ const config: ReturnType<typeof defineConfig> = {
|
|
|
19
19
|
window.__MICO_MENUS__ = ${JSON.stringify(mockMenus)};
|
|
20
20
|
window.__MICO_PAGES__ = ${JSON.stringify(mockPages)};
|
|
21
21
|
window.__MICO_CONFIG__ = {
|
|
22
|
+
appId: '<%= projectName %>',
|
|
22
23
|
appName: '<%= projectName %>',
|
|
23
24
|
title: '测试样例',
|
|
24
25
|
logo: '',
|
|
@@ -62,7 +63,6 @@ const config: ReturnType<typeof defineConfig> = {
|
|
|
62
63
|
},
|
|
63
64
|
define: {
|
|
64
65
|
'process.env.NODE_ENV': 'development',
|
|
65
|
-
'process.env.APP_ID': '<%= projectName %>',
|
|
66
66
|
'process.env.API_BASE_URL': '',
|
|
67
67
|
'process.env.PROXY_SUFFIX': '/proxy/<%= projectName %>_svr',
|
|
68
68
|
'process.env.LOGIN_ENDPOINT':
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
// https://umijs.org/config/
|
|
2
2
|
|
|
3
3
|
import { defineConfig } from '@umijs/max';
|
|
4
|
+
// 开发环境,不上传 sourcemap。调试时如有需要再解开
|
|
5
|
+
// import { applySentryPlugin } from "../../../scripts/apply-sentry-plugin";
|
|
4
6
|
|
|
5
7
|
const config: ReturnType<typeof defineConfig> = {
|
|
8
|
+
// 开发环境,不上传 sourcemap。调试时如有需要再解开
|
|
9
|
+
// devtool: 'hidden-source-map',
|
|
10
|
+
|
|
11
|
+
// chainWebpack(memo) {
|
|
12
|
+
// // 开发环境,不上传 sourcemap。调试时如有需要再解开
|
|
13
|
+
// applySentryPlugin({ memo, appName: 'layout' });
|
|
14
|
+
// return memo;
|
|
15
|
+
// },
|
|
16
|
+
|
|
6
17
|
externals: {
|
|
7
18
|
react: 'React',
|
|
8
19
|
'react-dom': 'ReactDOM',
|
|
@@ -10,7 +21,6 @@ const config: ReturnType<typeof defineConfig> = {
|
|
|
10
21
|
},
|
|
11
22
|
define: {
|
|
12
23
|
'process.env.NODE_ENV': 'development',
|
|
13
|
-
'process.env.APP_ID': '<%= projectName %>',
|
|
14
24
|
'process.env.API_BASE_URL': 'https://dashboard-api-test.micoplatform.com',
|
|
15
25
|
'process.env.PROXY_SUFFIX': '/proxy/<%= projectName %>_svr',
|
|
16
26
|
'process.env.LOGIN_ENDPOINT':
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
// https://umijs.org/config/
|
|
2
2
|
|
|
3
3
|
import { defineConfig } from '@umijs/max';
|
|
4
|
+
// 测试环境,不上传 sourcemap。调试时如有需要再解开
|
|
5
|
+
// import { applySentryPlugin } from "../../../scripts/apply-sentry-plugin";
|
|
4
6
|
|
|
5
7
|
const config: ReturnType<typeof defineConfig> = {
|
|
8
|
+
// 测试环境,不上传 sourcemap。调试时如有需要再解开
|
|
9
|
+
// devtool: 'hidden-source-map',
|
|
10
|
+
|
|
11
|
+
// chainWebpack(memo) {
|
|
12
|
+
// // 测试环境,不上传 sourcemap。调试时如有需要再解开
|
|
13
|
+
// applySentryPlugin({ memo, appName: 'layout' });
|
|
14
|
+
// return memo;
|
|
15
|
+
// },
|
|
16
|
+
|
|
6
17
|
externals: {
|
|
7
18
|
react: 'React',
|
|
8
19
|
'react-dom': 'ReactDOM',
|
|
@@ -10,7 +21,6 @@ const config: ReturnType<typeof defineConfig> = {
|
|
|
10
21
|
},
|
|
11
22
|
define: {
|
|
12
23
|
'process.env.NODE_ENV': 'testing',
|
|
13
|
-
'process.env.APP_ID': '<%= projectName %>',
|
|
14
24
|
'process.env.API_BASE_URL': 'https://dashboard-api-test.micoplatform.com',
|
|
15
25
|
'process.env.PROXY_SUFFIX': '/proxy/<%= projectName %>_svr',
|
|
16
26
|
'process.env.LOGIN_ENDPOINT':
|
|
@@ -16,7 +16,6 @@ const config: ReturnType<typeof defineConfig> = {
|
|
|
16
16
|
devtool: 'hidden-source-map',
|
|
17
17
|
define: {
|
|
18
18
|
'process.env.NODE_ENV': 'production',
|
|
19
|
-
'process.env.APP_ID': '<%= projectName %>',
|
|
20
19
|
'process.env.API_BASE_URL': 'https://dashboard-api.micoplatform.com',
|
|
21
20
|
'process.env.PROXY_SUFFIX': '/proxy/<%= projectName %>_svr',
|
|
22
21
|
'process.env.LOGIN_ENDPOINT':
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# 修复 SSO 无限重定向循环
|
|
2
|
+
|
|
3
|
+
> 创建时间:2026-03-12
|
|
4
|
+
|
|
5
|
+
## 功能概述
|
|
6
|
+
|
|
7
|
+
修复 SSO 登录流程中 `redirect_count` 防护机制被过早清除,导致 `fetchUserInfo` 返回 401 时可能触发无限 SSO 重定向循环的问题。
|
|
8
|
+
|
|
9
|
+
## 问题分析
|
|
10
|
+
|
|
11
|
+
### 原始流程(存在缺陷)
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
SSO 回跳(URL 含 redirect_count=1 & ticket)
|
|
15
|
+
→ ensureSsoSession() ticket 换 token 成功
|
|
16
|
+
→ 立即删除 redirect_count ← 问题根因
|
|
17
|
+
→ fetchUserInfo() 返回 401
|
|
18
|
+
→ refreshAuthToken() 失败
|
|
19
|
+
→ handleAuthFailureRedirect() 检查 redirect_count = 0(已被删除)
|
|
20
|
+
→ 允许重定向 → 跳转 SSO
|
|
21
|
+
→ SSO 自动登录回跳 → 重复以上流程 → 无限循环
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### 根因
|
|
25
|
+
|
|
26
|
+
`ensureSsoSession()` 在 ticket 换取 token 成功后**立即删除** URL 中的 `redirect_count` 参数,但后续的 `fetchUserInfo` 尚未执行。当 `fetchUserInfo` 返回 401 且 token 刷新失败时,`handleAuthFailureRedirect()` 因检测不到 `redirect_count` 而误判为首次重定向,触发循环。
|
|
27
|
+
|
|
28
|
+
## 技术方案
|
|
29
|
+
|
|
30
|
+
### 核心思路
|
|
31
|
+
|
|
32
|
+
将 `redirect_count` 的清除时机从"SSO ticket 换 token 成功后"延后到"整个认证流程(SSO + fetchUserInfo)完全成功后"。
|
|
33
|
+
|
|
34
|
+
### 修复后流程
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
SSO 回跳(URL 含 redirect_count=1 & ticket)
|
|
38
|
+
→ ensureSsoSession() ticket 换 token 成功
|
|
39
|
+
→ 保留 redirect_count ← 修复点
|
|
40
|
+
→ fetchUserInfo() 返回 401
|
|
41
|
+
→ refreshAuthToken() 失败
|
|
42
|
+
→ handleAuthFailureRedirect() 检查 redirect_count = 1
|
|
43
|
+
→ 已达最大重定向次数 → 停止重定向 ✅
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
SSO 回跳(URL 含 redirect_count=1 & ticket)
|
|
48
|
+
→ ensureSsoSession() ticket 换 token 成功
|
|
49
|
+
→ 保留 redirect_count
|
|
50
|
+
→ fetchUserInfo() 成功
|
|
51
|
+
→ clearRedirectCount() 清除 redirect_count ✅
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 文件清单
|
|
55
|
+
|
|
56
|
+
### 修改文件
|
|
57
|
+
|
|
58
|
+
| 文件路径 | 修改内容 |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| `src/common/request/sso.ts` | 移除 `ensureSsoSession` 中过早删除 `redirect_count` 的逻辑;新增 `clearRedirectCount()` 函数 |
|
|
61
|
+
| `src/app.tsx` | 导入 `clearRedirectCount`,在 `fetchUserInfo` 成功后调用 |
|
|
62
|
+
|
|
63
|
+
## API / 组件接口
|
|
64
|
+
|
|
65
|
+
### clearRedirectCount
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
export const clearRedirectCount = (): void
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
清除 URL 中的 `redirect_count` 参数。仅在整个认证流程完全成功后调用。
|
|
72
|
+
|
|
73
|
+
## 设计决策
|
|
74
|
+
|
|
75
|
+
| 决策点 | 选择 | 理由 |
|
|
76
|
+
| --- | --- | --- |
|
|
77
|
+
| redirect_count 清除时机 | 延后到 fetchUserInfo 成功后 | 最小改动,不改变现有 URL 参数机制,仅调整调用时序 |
|
|
78
|
+
| clearRedirectCount 放置位置 | `sso.ts` | 与 `handleAuthFailureRedirect` 同属 SSO 模块,保持内聚 |
|
|
79
|
+
|
|
80
|
+
## 注意事项
|
|
81
|
+
|
|
82
|
+
- `handleAuthFailureRedirect` 可能在 `request/index.ts`(refresh 失败时)和 `app.tsx`(fetchUserInfo 返回 null 时)被双重调用,但修复后两处检查到 `redirect_count >= 1` 均会停止重定向,不影响正确性
|
|
83
|
+
- `redirect_count` 最大值为 1,即只允许一次自动 SSO 重定向尝试
|
|
84
|
+
|
|
85
|
+
## 相关文档
|
|
86
|
+
|
|
87
|
+
- [请求模块架构](./arch-请求模块.md)
|
|
88
|
+
- [菜单权限控制](./feature-菜单权限控制.md)
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
setMicroAppProps,
|
|
18
18
|
} from './common/micro';
|
|
19
19
|
import {
|
|
20
|
+
clearRedirectCount,
|
|
20
21
|
ensureSsoSession,
|
|
21
22
|
handleAuthFailureRedirect,
|
|
22
23
|
} from './common/request/sso';
|
|
@@ -140,6 +141,7 @@ export async function getInitialState(): Promise<{
|
|
|
140
141
|
if (getStoredAuthToken()) {
|
|
141
142
|
const userInfo = await fetchUserInfoFn();
|
|
142
143
|
if (userInfo) {
|
|
144
|
+
clearRedirectCount();
|
|
143
145
|
return {
|
|
144
146
|
fetchUserInfo: fetchUserInfoFn,
|
|
145
147
|
currentUser: userInfo,
|
|
@@ -341,6 +341,10 @@ export const parseMenuItems = (items: MenuItem[]): ParsedMenuItem[] => {
|
|
|
341
341
|
});
|
|
342
342
|
};
|
|
343
343
|
|
|
344
|
+
/** 去除尾部斜杠(根路径 "/" 保持不变) */
|
|
345
|
+
const stripTrailingSlash = (path: string): string =>
|
|
346
|
+
path.length > 1 && path.endsWith('/') ? path.slice(0, -1) : path;
|
|
347
|
+
|
|
344
348
|
/**
|
|
345
349
|
* 根据路径查找对应的路由配置
|
|
346
350
|
*/
|
|
@@ -348,18 +352,19 @@ export const findRouteByPath = (
|
|
|
348
352
|
routes: ParsedRoute[],
|
|
349
353
|
pathname: string,
|
|
350
354
|
): ParsedRoute | undefined => {
|
|
355
|
+
const normalizedPathname = stripTrailingSlash(pathname);
|
|
351
356
|
let exact: ParsedRoute | undefined;
|
|
352
357
|
let bestWildcard: { route: ParsedRoute; basePath: string } | undefined;
|
|
353
358
|
|
|
354
359
|
for (const route of routes) {
|
|
355
|
-
if (route.path ===
|
|
360
|
+
if (stripTrailingSlash(route.path) === normalizedPathname) {
|
|
356
361
|
exact = route;
|
|
357
362
|
continue;
|
|
358
363
|
}
|
|
359
364
|
|
|
360
365
|
if (route.path.endsWith('/*')) {
|
|
361
366
|
const basePath = route.path.slice(0, -2);
|
|
362
|
-
if (
|
|
367
|
+
if (normalizedPathname === basePath || normalizedPathname.startsWith(`${basePath}/`)) {
|
|
363
368
|
if (!bestWildcard || basePath.length > bestWildcard.basePath.length) {
|
|
364
369
|
bestWildcard = { route, basePath };
|
|
365
370
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 请求客户端配置管理
|
|
3
|
-
* appId 优先从 window.__MICO_WORKSPACE__ 获取;
|
|
3
|
+
* appId 优先从 window.__MICO_WORKSPACE__ 获取,其次从 window.__MICO_CONFIG__ 获取;
|
|
4
4
|
* externalLoginPath 优先从 window.__MICO_WORKSPACE__.casServerLoginUrl 获取;
|
|
5
|
-
* 其余
|
|
5
|
+
* 其余 apiBaseUrl / proxySuffix / loginEndpoint / refreshEndpoint 优先从 window.__MICO_CONFIG__ 同名字段获取,兜底取 process.env 环境变量。
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { getStoredAuthToken } from '../auth/auth-manager';
|
|
@@ -20,7 +20,7 @@ function getMicoWorkspace(): Window['__MICO_WORKSPACE__'] {
|
|
|
20
20
|
|
|
21
21
|
function buildDefaultClientOptions(): RequestClientOptions {
|
|
22
22
|
const envDefaults: RequestClientOptions = {
|
|
23
|
-
appId:
|
|
23
|
+
appId: '',
|
|
24
24
|
ticketParam: 'ticket',
|
|
25
25
|
logoutPath: '/logout',
|
|
26
26
|
apiBaseUrl: process.env.API_BASE_URL ?? '',
|
|
@@ -164,7 +164,7 @@ export const initDefaultInterceptors = (
|
|
|
164
164
|
return data;
|
|
165
165
|
});
|
|
166
166
|
|
|
167
|
-
//
|
|
167
|
+
// 处理业务错误码({ result: { code, desc } } 格式)
|
|
168
168
|
addResponseInterceptor(async (data: unknown) => {
|
|
169
169
|
if (data && typeof data === 'object' && 'result' in data) {
|
|
170
170
|
const typedData = data as { result?: { code?: number; desc?: string } };
|
|
@@ -183,4 +183,32 @@ export const initDefaultInterceptors = (
|
|
|
183
183
|
}
|
|
184
184
|
return data;
|
|
185
185
|
});
|
|
186
|
+
|
|
187
|
+
// 处理业务错误码({ code, msg, data } 格式)
|
|
188
|
+
addResponseInterceptor(async (data: unknown) => {
|
|
189
|
+
if (
|
|
190
|
+
data &&
|
|
191
|
+
typeof data === 'object' &&
|
|
192
|
+
'code' in data &&
|
|
193
|
+
!('result' in data) &&
|
|
194
|
+
!('success' in data)
|
|
195
|
+
) {
|
|
196
|
+
const typedData = data as { code?: number | string; msg?: string };
|
|
197
|
+
const code = Number(typedData.code);
|
|
198
|
+
if (!isNaN(code) && code !== 200 && code !== 0) {
|
|
199
|
+
const error = new Error(
|
|
200
|
+
typedData.msg || `请求失败 (${code})`,
|
|
201
|
+
) as Error & {
|
|
202
|
+
code: number;
|
|
203
|
+
msg: string;
|
|
204
|
+
response: unknown;
|
|
205
|
+
};
|
|
206
|
+
error.code = code;
|
|
207
|
+
error.msg = typedData.msg || '';
|
|
208
|
+
error.response = data;
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return data;
|
|
213
|
+
});
|
|
186
214
|
};
|
|
@@ -113,16 +113,6 @@ export const ensureSsoSession = async (): Promise<void> => {
|
|
|
113
113
|
setFetchingToken(false);
|
|
114
114
|
|
|
115
115
|
if (resolveAuthToken()) {
|
|
116
|
-
// SSO 认证成功,删除 URL 中的 redirect_count 参数
|
|
117
|
-
if (typeof window !== 'undefined') {
|
|
118
|
-
const current = new URL(window.location.href);
|
|
119
|
-
current.searchParams.delete('redirect_count');
|
|
120
|
-
const nextUrl = `${current.pathname}${
|
|
121
|
-
current.search ? `?${current.searchParams}` : ''
|
|
122
|
-
}${current.hash}`;
|
|
123
|
-
window.history.replaceState(null, '', nextUrl);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
116
|
processPendingRequests();
|
|
127
117
|
} else {
|
|
128
118
|
throw new Error('SSO 认证失败');
|
|
@@ -143,3 +133,19 @@ export const ensureSsoSession = async (): Promise<void> => {
|
|
|
143
133
|
|
|
144
134
|
await ticketPromise;
|
|
145
135
|
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 清除 URL 中的 redirect_count 参数
|
|
139
|
+
* 仅在整个认证流程(SSO + fetchUserInfo)完全成功后调用,
|
|
140
|
+
* 避免过早清除导致无限重定向循环
|
|
141
|
+
*/
|
|
142
|
+
export const clearRedirectCount = (): void => {
|
|
143
|
+
if (typeof window === 'undefined') return;
|
|
144
|
+
const current = new URL(window.location.href);
|
|
145
|
+
if (!current.searchParams.has('redirect_count')) return;
|
|
146
|
+
current.searchParams.delete('redirect_count');
|
|
147
|
+
const nextUrl = `${current.pathname}${
|
|
148
|
+
current.search ? `?${current.searchParams}` : ''
|
|
149
|
+
}${current.hash}`;
|
|
150
|
+
window.history.replaceState(null, '', nextUrl);
|
|
151
|
+
};
|
package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx
CHANGED
|
@@ -86,6 +86,10 @@ const MicroAppLoader: React.FC<MicroAppLoaderProps> = ({
|
|
|
86
86
|
};
|
|
87
87
|
}, [env, routePath]);
|
|
88
88
|
|
|
89
|
+
// ref 持有最新的 buildProps,避免加载 effect 依赖它导致子应用被重载
|
|
90
|
+
const buildPropsRef = useRef(buildProps);
|
|
91
|
+
buildPropsRef.current = buildProps;
|
|
92
|
+
|
|
89
93
|
// 当 props 变化时,通知子应用更新(不重新加载)
|
|
90
94
|
useEffect(() => {
|
|
91
95
|
if (state.mounted) {
|
|
@@ -93,7 +97,7 @@ const MicroAppLoader: React.FC<MicroAppLoaderProps> = ({
|
|
|
93
97
|
}
|
|
94
98
|
}, [state.mounted, buildProps]);
|
|
95
99
|
|
|
96
|
-
//
|
|
100
|
+
// 加载微应用(只在 entry/appName/isAuthReady 变化时触发)
|
|
97
101
|
useEffect(() => {
|
|
98
102
|
const wrapper = wrapperRef.current;
|
|
99
103
|
if (!wrapper) {
|
|
@@ -117,7 +121,7 @@ const MicroAppLoader: React.FC<MicroAppLoaderProps> = ({
|
|
|
117
121
|
name: appName,
|
|
118
122
|
entry,
|
|
119
123
|
target: wrapper,
|
|
120
|
-
props:
|
|
124
|
+
props: buildPropsRef.current(),
|
|
121
125
|
});
|
|
122
126
|
|
|
123
127
|
return () => {
|
|
@@ -125,7 +129,7 @@ const MicroAppLoader: React.FC<MicroAppLoaderProps> = ({
|
|
|
125
129
|
microAppManager.cancel();
|
|
126
130
|
microAppManager.setStateCallback(null);
|
|
127
131
|
};
|
|
128
|
-
}, [entry, appName, isAuthReady
|
|
132
|
+
}, [entry, appName, isAuthReady]);
|
|
129
133
|
|
|
130
134
|
return (
|
|
131
135
|
<div className="micro-app-container">
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@commitlint/cli": "^19.5.0",
|
|
34
34
|
"@commitlint/config-conventional": "^19.5.0",
|
|
35
|
+
"@common-web/sentry": "^0.0.4",
|
|
35
36
|
"@sentry/webpack-plugin": "^4.9.1",
|
|
36
37
|
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
|
37
38
|
"@typescript-eslint/parser": "^8.54.0",
|
|
@@ -32,7 +32,7 @@ export function applySentryPlugin({
|
|
|
32
32
|
name: version,
|
|
33
33
|
uploadLegacySourcemaps: {
|
|
34
34
|
paths: ['./dist'],
|
|
35
|
-
urlPrefix:
|
|
35
|
+
urlPrefix: `~/<%= cdnPrefixPath %><%= projectName %>/${version}/${appName}`,
|
|
36
36
|
},
|
|
37
37
|
},
|
|
38
38
|
sourcemaps: {
|
|
@@ -48,6 +48,16 @@ async function collectDist() {
|
|
|
48
48
|
|
|
49
49
|
// 移动 dist 目录到目标位置
|
|
50
50
|
await fs.rename(appDistPath, targetDistPath);
|
|
51
|
+
|
|
52
|
+
// 删除 .map 文件(sourcemap 已由 webpack 插件上传到 Sentry,无需部署到 CDN)
|
|
53
|
+
const files = await fs.readdir(targetDistPath);
|
|
54
|
+
for (const file of files) {
|
|
55
|
+
if (file.endsWith('.map')) {
|
|
56
|
+
await fs.unlink(path.join(targetDistPath, file));
|
|
57
|
+
console.log(` 🗑 已删除 sourcemap: ${appName}/${file}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
51
61
|
console.log(`✓ ${appName}: apps/${appName}/dist -> dist/${appName}/`);
|
|
52
62
|
} catch (error) {
|
|
53
63
|
if (error.code === 'ENOENT') {
|
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
transformDestPath,
|
|
11
11
|
isTemplateFile,
|
|
12
12
|
getPackageVersionsParallel,
|
|
13
|
+
detectPackageScope,
|
|
13
14
|
setupErrorHandlers,
|
|
14
15
|
createLogger,
|
|
15
16
|
loadMicorc,
|
|
@@ -61,24 +62,6 @@ module.exports = class extends Generator {
|
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
_detectPackageScope() {
|
|
65
|
-
// 从根 package.json 的 name 推断 scope(与 micro-react 生成的项目一致)
|
|
66
|
-
const rootPkgPath = path.join(this.monorepoRoot, 'package.json');
|
|
67
|
-
if (fs.existsSync(rootPkgPath)) {
|
|
68
|
-
try {
|
|
69
|
-
const pkg = JSON.parse(fs.readFileSync(rootPkgPath, 'utf-8'));
|
|
70
|
-
if (pkg.name) {
|
|
71
|
-
return pkg.name.startsWith('@')
|
|
72
|
-
? pkg.name.replace(/^(@[^/]+)\/.*/, '$1')
|
|
73
|
-
: `@${pkg.name}`;
|
|
74
|
-
}
|
|
75
|
-
} catch (e) {
|
|
76
|
-
// 忽略解析错误
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
65
|
async prompting() {
|
|
83
66
|
try {
|
|
84
67
|
if (!this.monorepoRoot) {
|
|
@@ -88,7 +71,7 @@ module.exports = class extends Generator {
|
|
|
88
71
|
process.exit(1);
|
|
89
72
|
}
|
|
90
73
|
|
|
91
|
-
const detectedScope = this.
|
|
74
|
+
const detectedScope = detectPackageScope(this.monorepoRoot);
|
|
92
75
|
const rc = this.rcConfig || {};
|
|
93
76
|
|
|
94
77
|
this.answers = await this.prompt([
|
|
@@ -349,7 +332,7 @@ module.exports = class extends Generator {
|
|
|
349
332
|
' accessControlEnabled: false,',
|
|
350
333
|
' adminOnly: false,',
|
|
351
334
|
' routeKey: null,',
|
|
352
|
-
` mainDocumentId: ${
|
|
335
|
+
` mainDocumentId: ${maxId + 1},`,
|
|
353
336
|
" version: '',",
|
|
354
337
|
' },',
|
|
355
338
|
].join('\n');
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
// https://umijs.org/config/
|
|
2
2
|
|
|
3
3
|
import { defineConfig } from '@umijs/max';
|
|
4
|
+
// 开发环境,不上传 sourcemap。调试时如有需要再解开
|
|
5
|
+
// import { applySentryPlugin } from "../../../scripts/apply-sentry-plugin";
|
|
4
6
|
|
|
5
7
|
const config: ReturnType<typeof defineConfig> = {
|
|
8
|
+
// 开发环境,不上传 sourcemap。调试时如有需要再解开
|
|
9
|
+
// devtool: 'hidden-source-map',
|
|
10
|
+
|
|
6
11
|
// 测试环境:将所有代码打包到一个文件
|
|
7
12
|
extraBabelPlugins: ['babel-plugin-dynamic-import-node'],
|
|
8
13
|
|
|
@@ -12,6 +17,8 @@ const config: ReturnType<typeof defineConfig> = {
|
|
|
12
17
|
memo.optimization.splitChunks(false);
|
|
13
18
|
// 禁用 runtimeChunk
|
|
14
19
|
memo.optimization.runtimeChunk(false);
|
|
20
|
+
// 开发环境,不上传 sourcemap。调试时如有需要再解开
|
|
21
|
+
// applySentryPlugin({ memo, appName: '<%= appName %>' });
|
|
15
22
|
return memo;
|
|
16
23
|
},
|
|
17
24
|
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
// https://umijs.org/config/
|
|
2
2
|
|
|
3
3
|
import { defineConfig } from '@umijs/max';
|
|
4
|
+
// 测试环境,不上传 sourcemap。调试时如有需要再解开
|
|
5
|
+
// import { applySentryPlugin } from "../../../scripts/apply-sentry-plugin";
|
|
4
6
|
|
|
5
7
|
const config: ReturnType<typeof defineConfig> = {
|
|
8
|
+
// 测试环境,不上传 sourcemap。调试时如有需要再解开
|
|
9
|
+
// devtool: 'hidden-source-map',
|
|
10
|
+
|
|
6
11
|
// 测试环境:将所有代码打包到一个文件
|
|
7
12
|
extraBabelPlugins: ['babel-plugin-dynamic-import-node'],
|
|
8
13
|
|
|
@@ -12,6 +17,8 @@ const config: ReturnType<typeof defineConfig> = {
|
|
|
12
17
|
memo.optimization.splitChunks(false);
|
|
13
18
|
// 禁用 runtimeChunk
|
|
14
19
|
memo.optimization.runtimeChunk(false);
|
|
20
|
+
// 测试环境,不上传 sourcemap。调试时如有需要再解开
|
|
21
|
+
// applySentryPlugin({ memo, appName: '<%= appName %>' });
|
|
15
22
|
return memo;
|
|
16
23
|
},
|
|
17
24
|
|
|
@@ -15,13 +15,13 @@
|
|
|
15
15
|
"start": "npm run dev"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
+
"@mico-platform/ui": "<%= micoUiVersion %>",
|
|
18
19
|
"@mico-platform/theme": "<%= themeVersion %>",
|
|
19
20
|
"@umijs/max": "^4.4.8",
|
|
20
21
|
"react": "^18.2.0",
|
|
21
22
|
"react-dom": "^18.2.0"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
|
-
"@common-web/sentry": "^0.0.3",
|
|
25
25
|
"@mico-platform/ui": "<%= micoUiVersion %>",
|
|
26
26
|
"@types/react": "^18.0.33",
|
|
27
27
|
"@types/react-dom": "^18.0.11",
|
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
collectFiles,
|
|
11
11
|
transformDestPath,
|
|
12
12
|
isTemplateFile,
|
|
13
|
+
detectPackageScope,
|
|
13
14
|
setupErrorHandlers,
|
|
14
15
|
createLogger,
|
|
15
16
|
loadMicorc,
|
|
@@ -53,26 +54,9 @@ module.exports = class extends Generator {
|
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
_detectPackageScope() {
|
|
57
|
-
const rootPkgPath = path.join(this.monorepoRoot, 'package.json');
|
|
58
|
-
if (fs.existsSync(rootPkgPath)) {
|
|
59
|
-
try {
|
|
60
|
-
const pkg = JSON.parse(fs.readFileSync(rootPkgPath, 'utf-8'));
|
|
61
|
-
if (pkg.name) {
|
|
62
|
-
return pkg.name.startsWith('@')
|
|
63
|
-
? pkg.name.replace(/^(@[^/]+)\/.*/, '$1')
|
|
64
|
-
: `@${pkg.name}`;
|
|
65
|
-
}
|
|
66
|
-
} catch (e) {
|
|
67
|
-
// ignore
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
57
|
async prompting() {
|
|
74
58
|
try {
|
|
75
|
-
const detectedScope = this.
|
|
59
|
+
const detectedScope = detectPackageScope(this.monorepoRoot);
|
|
76
60
|
const rc = this.rcConfig || {};
|
|
77
61
|
|
|
78
62
|
this.answers = await this.prompt([
|
|
@@ -57,8 +57,10 @@ module.exports = (env, argv) => {
|
|
|
57
57
|
port: <%= devPort %>,
|
|
58
58
|
hot: true,
|
|
59
59
|
open: false,
|
|
60
|
+
allowedHosts: 'all',
|
|
60
61
|
headers: {
|
|
61
62
|
'Access-Control-Allow-Origin': '*',
|
|
63
|
+
'Access-Control-Allow-Methods': 'GET',
|
|
62
64
|
},
|
|
63
65
|
static: {
|
|
64
66
|
directory: path.join(__dirname, 'public'),
|
package/lib/utils.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const path = require('node:path');
|
|
5
|
-
const { execSync,
|
|
5
|
+
const { execSync, execFileSync, execFile } = require('node:child_process');
|
|
6
6
|
const os = require('node:os');
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -122,6 +122,31 @@ function isTemplateFile(filePath) {
|
|
|
122
122
|
return TEMPLATE_EXTENSIONS.has(path.extname(filePath));
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/**
|
|
126
|
+
* 从 monorepo 根目录的 package.json 推断包 scope
|
|
127
|
+
* @param {string} monorepoRoot - monorepo 根目录路径
|
|
128
|
+
* @returns {string|null} 推断出的 scope(如 '@my-project'),未找到返回 null
|
|
129
|
+
*/
|
|
130
|
+
function detectPackageScope(monorepoRoot) {
|
|
131
|
+
const rootPkgPath = path.join(monorepoRoot, 'package.json');
|
|
132
|
+
if (fs.existsSync(rootPkgPath)) {
|
|
133
|
+
try {
|
|
134
|
+
const pkg = JSON.parse(fs.readFileSync(rootPkgPath, 'utf-8'));
|
|
135
|
+
if (pkg.name) {
|
|
136
|
+
return pkg.name.startsWith('@')
|
|
137
|
+
? pkg.name.replace(/^(@[^/]+)\/.*/, '$1')
|
|
138
|
+
: `@${pkg.name}`;
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
// ignore
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** 合法 npm 包名正则:可选 @scope/name,仅允许字母数字、连字符、点、下划线 */
|
|
148
|
+
const VALID_PACKAGE_NAME_RE = /^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
|
|
149
|
+
|
|
125
150
|
/** scope 包含 mico 时使用的 registry(如 @mico-platform/*) */
|
|
126
151
|
const MICO_NPM_REGISTRY = 'https://nexus-vywrajy.micoworld.net/repository/mico-base-common/';
|
|
127
152
|
|
|
@@ -146,11 +171,14 @@ function getRegistryForPackage(packageName) {
|
|
|
146
171
|
* @returns {string} 版本号,如 '1.2.3'
|
|
147
172
|
*/
|
|
148
173
|
function getLatestNpmVersion(packageName, fallback = '1.0.0', timeoutMs = 8000, cwd) {
|
|
174
|
+
if (!VALID_PACKAGE_NAME_RE.test(packageName)) {
|
|
175
|
+
return fallback;
|
|
176
|
+
}
|
|
149
177
|
const registry = getRegistryForPackage(packageName);
|
|
150
|
-
const
|
|
178
|
+
const args = ['view', packageName, 'version'];
|
|
179
|
+
if (registry) args.push(`--registry=${registry}`);
|
|
151
180
|
try {
|
|
152
|
-
const
|
|
153
|
-
const out = execSync(cmd, {
|
|
181
|
+
const out = execFileSync('npm', args, {
|
|
154
182
|
encoding: 'utf-8',
|
|
155
183
|
timeout: timeoutMs,
|
|
156
184
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -172,28 +200,38 @@ function getLatestNpmVersion(packageName, fallback = '1.0.0', timeoutMs = 8000,
|
|
|
172
200
|
* @returns {Promise<string>} 版本号,如 '1.2.3'
|
|
173
201
|
*/
|
|
174
202
|
function getLatestNpmVersionAsync(packageName, fallback = '1.0.0', timeoutMs = 8000, cwd) {
|
|
203
|
+
if (!VALID_PACKAGE_NAME_RE.test(packageName)) {
|
|
204
|
+
return Promise.resolve(fallback);
|
|
205
|
+
}
|
|
175
206
|
return new Promise((resolve) => {
|
|
207
|
+
let settled = false;
|
|
208
|
+
const done = (value) => {
|
|
209
|
+
if (!settled) {
|
|
210
|
+
settled = true;
|
|
211
|
+
resolve(value);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
176
215
|
const registry = getRegistryForPackage(packageName);
|
|
177
|
-
const
|
|
178
|
-
|
|
216
|
+
const args = ['view', packageName, 'version'];
|
|
217
|
+
if (registry) args.push(`--registry=${registry}`);
|
|
179
218
|
|
|
180
|
-
const child =
|
|
219
|
+
const child = execFile('npm', args, {
|
|
181
220
|
encoding: 'utf-8',
|
|
182
221
|
timeout: timeoutMs,
|
|
183
222
|
...(cwd && { cwd })
|
|
184
223
|
}, (error, stdout) => {
|
|
185
224
|
if (error) {
|
|
186
|
-
|
|
225
|
+
done(fallback);
|
|
187
226
|
return;
|
|
188
227
|
}
|
|
189
228
|
const v = stdout.trim();
|
|
190
|
-
|
|
229
|
+
done(v && /^\d+\.\d+\.\d+/.test(v) ? v : fallback);
|
|
191
230
|
});
|
|
192
231
|
|
|
193
|
-
// 确保超时后也能 resolve
|
|
194
232
|
setTimeout(() => {
|
|
195
233
|
child.kill();
|
|
196
|
-
|
|
234
|
+
done(fallback);
|
|
197
235
|
}, timeoutMs + 100);
|
|
198
236
|
});
|
|
199
237
|
}
|
|
@@ -277,8 +315,7 @@ function mergeDefaults(defaults, rcConfig) {
|
|
|
277
315
|
*/
|
|
278
316
|
function checkCommand(command) {
|
|
279
317
|
try {
|
|
280
|
-
const
|
|
281
|
-
const out = execSync(`${command} ${versionFlag}`, {
|
|
318
|
+
const out = execSync(`${command} --version`, {
|
|
282
319
|
encoding: 'utf-8',
|
|
283
320
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
284
321
|
timeout: 5000
|
|
@@ -396,8 +433,11 @@ function createLogger(generator) {
|
|
|
396
433
|
|
|
397
434
|
/**
|
|
398
435
|
* 安装全局未捕获异常处理
|
|
436
|
+
* 在 Vitest 等测试环境中自动跳过,避免干扰测试 runner 的进程管理
|
|
399
437
|
*/
|
|
400
438
|
function setupErrorHandlers() {
|
|
439
|
+
if (process.env.VITEST) return;
|
|
440
|
+
|
|
401
441
|
process.on('uncaughtException', (error) => {
|
|
402
442
|
console.error('');
|
|
403
443
|
console.error('❌ Unexpected error:');
|
|
@@ -431,6 +471,7 @@ module.exports = {
|
|
|
431
471
|
toPascal,
|
|
432
472
|
toCamel,
|
|
433
473
|
// Generator 工具
|
|
474
|
+
detectPackageScope,
|
|
434
475
|
shouldIgnorePath,
|
|
435
476
|
collectFiles,
|
|
436
477
|
transformDestPath,
|
package/package.json
CHANGED
|
@@ -1,22 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "generator-mico-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.25",
|
|
4
4
|
"description": "Yeoman generator for Mico CLI projects",
|
|
5
|
-
"keywords": [
|
|
5
|
+
"keywords": [
|
|
6
|
+
"yeoman-generator",
|
|
7
|
+
"generator",
|
|
8
|
+
"cli"
|
|
9
|
+
],
|
|
6
10
|
"license": "MIT",
|
|
7
11
|
"main": "generators/subapp-react/index.js",
|
|
8
12
|
"bin": {
|
|
9
13
|
"mico": "bin/mico.js"
|
|
10
14
|
},
|
|
11
|
-
"files": [
|
|
15
|
+
"files": [
|
|
16
|
+
"bin",
|
|
17
|
+
"lib",
|
|
18
|
+
"generators"
|
|
19
|
+
],
|
|
12
20
|
"scripts": {
|
|
13
21
|
"format": "biome format --write .",
|
|
14
22
|
"format:check": "biome format .",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"test:watch": "vitest",
|
|
15
25
|
"sync:subapp-react-template": "node scripts/sync-subapp-react-template.js",
|
|
16
26
|
"verify:micro-react": "node scripts/verify-micro-react.js"
|
|
17
27
|
},
|
|
18
28
|
"devDependencies": {
|
|
19
|
-
"@biomejs/biome": "^1.9.4"
|
|
29
|
+
"@biomejs/biome": "^1.9.4",
|
|
30
|
+
"vitest": "^4.0.18",
|
|
31
|
+
"yeoman-environment": "^3.19.3",
|
|
32
|
+
"yeoman-test": "^6.3.0"
|
|
20
33
|
},
|
|
21
34
|
"engines": {
|
|
22
35
|
"node": ">=18"
|