generator-mico-cli 0.2.31 → 0.2.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/README.md +145 -18
  2. package/bin/mico.js +76 -0
  3. package/generators/h5-react/ignore-list.json +1 -0
  4. package/generators/h5-react/index.js +349 -0
  5. package/generators/h5-react/meta.json +11 -0
  6. package/generators/h5-react/templates/.commitlintrc.js +7 -0
  7. package/generators/h5-react/templates/.cursor/rules/cicd-deploy.mdc +104 -0
  8. package/generators/h5-react/templates/.cursor/rules/common-intl.mdc +42 -0
  9. package/generators/h5-react/templates/.cursor/rules/git-hooks.mdc +40 -0
  10. package/generators/h5-react/templates/.cursor/rules/internal-packages.mdc +46 -0
  11. package/generators/h5-react/templates/.cursor/rules/monorepo.mdc +64 -0
  12. package/generators/h5-react/templates/.cursor/rules/package-json.mdc +52 -0
  13. package/generators/h5-react/templates/.cursor/rules/tailwind-umi.mdc +60 -0
  14. package/generators/h5-react/templates/.cursor/rules/umi-app.mdc +74 -0
  15. package/generators/h5-react/templates/.cursor/rules/umi-config.mdc +86 -0
  16. package/generators/h5-react/templates/.cursor/rules/umi-mock.mdc +80 -0
  17. package/generators/h5-react/templates/.cursor/rules/workspace-request.mdc +52 -0
  18. package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/SKILL.md +213 -0
  19. package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/evals/evals.json +23 -0
  20. package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/references/cursor-rule-template.md +60 -0
  21. package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/references/phase-1-scanning.md +102 -0
  22. package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/references/phase-2-context-analysis.md +102 -0
  23. package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/references/phase-3-pattern-extraction.md +105 -0
  24. package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/references/phase-4-module-mapping.md +65 -0
  25. package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/references/phase-5-glossary.md +63 -0
  26. package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/references/templates/DEV_PATTERNS.tpl.md +77 -0
  27. package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/references/templates/GLOSSARY.tpl.md +17 -0
  28. package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/references/templates/MODULE_MAP.tpl.md +45 -0
  29. package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/references/templates/PROJECT_CONTEXT.tpl.md +155 -0
  30. package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/references/update-mode.md +116 -0
  31. package/generators/h5-react/templates/.env.development +5 -0
  32. package/generators/h5-react/templates/.env.production +5 -0
  33. package/generators/h5-react/templates/.env.testing +5 -0
  34. package/generators/h5-react/templates/.husky/commit-msg +2 -0
  35. package/generators/h5-react/templates/.husky/pre-commit +2 -0
  36. package/generators/h5-react/templates/.lintstagedrc.js +8 -0
  37. package/generators/h5-react/templates/.prettierrc.json +7 -0
  38. package/generators/h5-react/templates/CICD/before_build.sh +76 -0
  39. package/generators/h5-react/templates/CICD/start_dev.sh +54 -0
  40. package/generators/h5-react/templates/CICD/start_local.sh +30 -0
  41. package/generators/h5-react/templates/CICD/start_prod.sh +53 -0
  42. package/generators/h5-react/templates/CICD/start_test.sh +55 -0
  43. package/generators/h5-react/templates/CICD/wangsu_fresh_dev.sh +19 -0
  44. package/generators/h5-react/templates/CICD/wangsu_fresh_prod.sh +19 -0
  45. package/generators/h5-react/templates/CICD/wangsu_fresh_test.sh +19 -0
  46. package/generators/h5-react/templates/README.md +301 -0
  47. package/generators/h5-react/templates/_gitignore +30 -0
  48. package/generators/h5-react/templates/_npmrc +6 -0
  49. package/generators/h5-react/templates/apps/.gitkeep +0 -0
  50. package/generators/h5-react/templates/dev.preset.json +10 -0
  51. package/generators/h5-react/templates/package.json +56 -0
  52. package/generators/h5-react/templates/packages/common-intl/README.md +180 -0
  53. package/generators/h5-react/templates/packages/common-intl/eslint.config.ts +12 -0
  54. package/generators/h5-react/templates/packages/common-intl/package.json +31 -0
  55. package/generators/h5-react/templates/packages/common-intl/src/index.ts +3 -0
  56. package/generators/h5-react/templates/packages/common-intl/src/intl.ts +100 -0
  57. package/generators/h5-react/templates/packages/common-intl/tsconfig.json +3 -0
  58. package/generators/h5-react/templates/packages/components/eslint.config.ts +12 -0
  59. package/generators/h5-react/templates/packages/components/package.json +32 -0
  60. package/generators/h5-react/templates/packages/components/src/Layout/ImmersiveHeader.tsx +126 -0
  61. package/generators/h5-react/templates/packages/components/src/Layout/LayoutFooter.tsx +72 -0
  62. package/generators/h5-react/templates/packages/components/src/Layout/index.tsx +121 -0
  63. package/generators/h5-react/templates/packages/components/src/assets/image/back.png +0 -0
  64. package/generators/h5-react/templates/packages/components/src/index.ts +0 -0
  65. package/generators/h5-react/templates/packages/components/tsconfig.json +13 -0
  66. package/generators/h5-react/templates/packages/components/typings.d.ts +1 -0
  67. package/generators/h5-react/templates/packages/constant/eslint.config.ts +12 -0
  68. package/generators/h5-react/templates/packages/constant/package.json +19 -0
  69. package/generators/h5-react/templates/packages/constant/src/index.ts +0 -0
  70. package/generators/h5-react/templates/packages/constant/src/member.ts +8 -0
  71. package/generators/h5-react/templates/packages/constant/tsconfig.json +3 -0
  72. package/generators/h5-react/templates/packages/deeplink/eslint.config.ts +12 -0
  73. package/generators/h5-react/templates/packages/deeplink/package.json +18 -0
  74. package/generators/h5-react/templates/packages/deeplink/src/index.ts +7 -0
  75. package/generators/h5-react/templates/packages/deeplink/tsconfig.json +3 -0
  76. package/generators/h5-react/templates/packages/domain/eslint.config.ts +12 -0
  77. package/generators/h5-react/templates/packages/domain/package.json +18 -0
  78. package/generators/h5-react/templates/packages/domain/src/index.ts +29 -0
  79. package/generators/h5-react/templates/packages/domain/tsconfig.json +3 -0
  80. package/generators/h5-react/templates/packages/domain/types.d.ts +11 -0
  81. package/generators/h5-react/templates/packages/eslint/eslint.config.base.ts +36 -0
  82. package/generators/h5-react/templates/packages/eslint/eslint.config.react.ts +33 -0
  83. package/generators/h5-react/templates/packages/eslint/package.json +22 -0
  84. package/generators/h5-react/templates/packages/js-bridge/eslint.config.ts +17 -0
  85. package/generators/h5-react/templates/packages/js-bridge/package.json +23 -0
  86. package/generators/h5-react/templates/packages/js-bridge/src/call.ts +126 -0
  87. package/generators/h5-react/templates/packages/js-bridge/src/closeH5Page.ts +9 -0
  88. package/generators/h5-react/templates/packages/js-bridge/src/getUserInfo.ts +96 -0
  89. package/generators/h5-react/templates/packages/js-bridge/src/index.ts +15 -0
  90. package/generators/h5-react/templates/packages/js-bridge/tsconfig.json +3 -0
  91. package/generators/h5-react/templates/packages/js-bridge/type.d.ts +24 -0
  92. package/generators/h5-react/templates/packages/request/axios.d.ts +42 -0
  93. package/generators/h5-react/templates/packages/request/eslint.config.ts +17 -0
  94. package/generators/h5-react/templates/packages/request/package.json +22 -0
  95. package/generators/h5-react/templates/packages/request/src/index.ts +165 -0
  96. package/generators/h5-react/templates/packages/request/src/interceptors.ts +126 -0
  97. package/generators/h5-react/templates/packages/request/src/types.ts +101 -0
  98. package/generators/h5-react/templates/packages/request/src/url-resolver.ts +66 -0
  99. package/generators/h5-react/templates/packages/request/src/utils.ts +12 -0
  100. package/generators/h5-react/templates/packages/request/tsconfig.json +3 -0
  101. package/generators/h5-react/templates/packages/request/umi.d.ts +94 -0
  102. package/generators/h5-react/templates/packages/typescript/package.json +11 -0
  103. package/generators/h5-react/templates/packages/typescript/tsconfig.base.json +23 -0
  104. package/generators/h5-react/templates/packages/typescript/tsconfig.react.json +7 -0
  105. package/generators/h5-react/templates/packages/umi-config/eslint.config.ts +12 -0
  106. package/generators/h5-react/templates/packages/umi-config/package.json +31 -0
  107. package/generators/h5-react/templates/packages/umi-config/src/config.dev.ts +34 -0
  108. package/generators/h5-react/templates/packages/umi-config/src/config.prod.development.ts +17 -0
  109. package/generators/h5-react/templates/packages/umi-config/src/config.prod.production.ts +42 -0
  110. package/generators/h5-react/templates/packages/umi-config/src/config.prod.testing.ts +17 -0
  111. package/generators/h5-react/templates/packages/umi-config/src/config.prod.ts +56 -0
  112. package/generators/h5-react/templates/packages/umi-config/src/config.ts +86 -0
  113. package/generators/h5-react/templates/packages/umi-config/src/index.ts +25 -0
  114. package/generators/h5-react/templates/packages/umi-config/src/plugins/apply-sentry-plugin.ts +57 -0
  115. package/generators/h5-react/templates/packages/umi-config/src/type.d.ts +3 -0
  116. package/generators/h5-react/templates/packages/umi-config/tsconfig.json +3 -0
  117. package/generators/h5-react/templates/packages/utils/eslint.config.ts +12 -0
  118. package/generators/h5-react/templates/packages/utils/package.json +27 -0
  119. package/generators/h5-react/templates/packages/utils/src/date.ts +21 -0
  120. package/generators/h5-react/templates/packages/utils/src/env.ts +40 -0
  121. package/generators/h5-react/templates/packages/utils/src/index.ts +3 -0
  122. package/generators/h5-react/templates/packages/utils/src/md5.ts +17 -0
  123. package/generators/h5-react/templates/packages/utils/src/mock.ts +83 -0
  124. package/generators/h5-react/templates/packages/utils/src/number.ts +23 -0
  125. package/generators/h5-react/templates/packages/utils/src/tailwind.ts +12 -0
  126. package/generators/h5-react/templates/packages/utils/src/url.ts +19 -0
  127. package/generators/h5-react/templates/packages/utils/tsconfig.json +9 -0
  128. package/generators/h5-react/templates/page.config.ts +1 -0
  129. package/generators/h5-react/templates/pnpm-workspace.yaml +17 -0
  130. package/generators/h5-react/templates/scripts/collect-dist.js +78 -0
  131. package/generators/h5-react/templates/scripts/dev-preset.js +265 -0
  132. package/generators/h5-react/templates/scripts/dev-preset.schema.json +39 -0
  133. package/generators/h5-react/templates/scripts/dev.js +133 -0
  134. package/generators/h5-react/templates/scripts/gateway.ts +241 -0
  135. package/generators/h5-react/templates/turbo.json +86 -0
  136. package/generators/micro-react/templates/apps/layout/config/config.dev.ts +2 -0
  137. package/generators/micro-react/templates/apps/layout/docs/feature-/345/244/264/351/203/250/344/270/200/347/272/247/345/257/274/350/210/252/345/270/203/345/261/200.md +166 -0
  138. package/generators/micro-react/templates/apps/layout/src/app.tsx +20 -2
  139. package/generators/micro-react/templates/apps/layout/src/common/menu/parser.ts +36 -0
  140. package/generators/micro-react/templates/apps/layout/src/common/menu/types.ts +6 -0
  141. package/generators/micro-react/templates/apps/layout/src/common/request/index.ts +8 -2
  142. package/generators/micro-react/templates/apps/layout/src/common/request/sso.ts +128 -11
  143. package/generators/micro-react/templates/apps/layout/src/constants/index.ts +22 -0
  144. package/generators/micro-react/templates/apps/layout/src/hooks/useFilteredMenuItems.ts +29 -0
  145. package/generators/micro-react/templates/apps/layout/src/layouts/components/header/index.less +73 -0
  146. package/generators/micro-react/templates/apps/layout/src/layouts/components/header/index.tsx +51 -4
  147. package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.tsx +25 -18
  148. package/generators/micro-react/templates/apps/layout/src/locales/en-US.ts +4 -0
  149. package/generators/micro-react/templates/apps/layout/src/locales/zh-CN.ts +4 -0
  150. package/generators/micro-react/templates/apps/layout/src/services/config/index.ts +1 -0
  151. package/generators/micro-react/templates/packages/common-intl/src/intl.ts +20 -0
  152. package/generators/subapp-h5/ignore-list.json +1 -0
  153. package/generators/subapp-h5/index.js +424 -0
  154. package/generators/subapp-h5/meta.json +10 -0
  155. package/generators/subapp-h5/templates/.env +1 -0
  156. package/generators/subapp-h5/templates/.stylelintrc.js +22 -0
  157. package/generators/subapp-h5/templates/config/config.dev.ts +7 -0
  158. package/generators/subapp-h5/templates/config/config.prod.development.ts +7 -0
  159. package/generators/subapp-h5/templates/config/config.prod.production.ts +10 -0
  160. package/generators/subapp-h5/templates/config/config.prod.testing.ts +7 -0
  161. package/generators/subapp-h5/templates/config/config.prod.ts +7 -0
  162. package/generators/subapp-h5/templates/config/config.ts +6 -0
  163. package/generators/subapp-h5/templates/config/routes.ts +13 -0
  164. package/generators/subapp-h5/templates/eslint.config.ts +12 -0
  165. package/generators/subapp-h5/templates/mock/user.ts +34 -0
  166. package/generators/subapp-h5/templates/package.json +42 -0
  167. package/generators/subapp-h5/templates/src/app.tsx +14 -0
  168. package/generators/subapp-h5/templates/src/assets/yay.jpg +0 -0
  169. package/generators/subapp-h5/templates/src/intl.ts +37 -0
  170. package/generators/subapp-h5/templates/src/layouts/index.tsx +10 -0
  171. package/generators/subapp-h5/templates/src/pages/index.tsx +22 -0
  172. package/generators/subapp-h5/templates/src/services/user.ts +38 -0
  173. package/generators/subapp-h5/templates/tailwind.config.js +16 -0
  174. package/generators/subapp-h5/templates/tailwind.css +7 -0
  175. package/generators/subapp-h5/templates/tsconfig.json +3 -0
  176. package/generators/subapp-h5/templates/typings.d.ts +1 -0
  177. package/lib/setup-multica-desktop.js +154 -0
  178. package/lib/utils.js +25 -0
  179. package/package.json +1 -1
@@ -0,0 +1,241 @@
1
+ /**
2
+ * 本地开发网关脚本:读取根目录 `page.config.ts`,为每个 `apps/<key>` 分配端口并启动 `pnpm dev`,
3
+ * 再启动统一 HTTP/WebSocket 代理(默认从 3000 起占端口),通过路径前缀将流量转发到对应子应用。
4
+ */
5
+ import http from 'http';
6
+ import path from 'path';
7
+ import fs from 'fs';
8
+ import { spawn, type ChildProcessWithoutNullStreams } from 'child_process';
9
+ import httpProxy from 'http-proxy';
10
+ import portfinder from 'portfinder';
11
+ import chalk from 'chalk';
12
+
13
+ /**
14
+ * 仓库根目录的 `page.config.ts`:键名为子应用目录名(对应 `apps/<key>`)。
15
+ */
16
+ import pageConfig from '../page.config.ts';
17
+
18
+ /** 单个子应用在网关中的运行时元数据(端口、路径前缀等)。 */
19
+ interface PageConfig {
20
+ key: string;
21
+ name: string;
22
+ fullPath: string;
23
+ prefixPath: string;
24
+ port: number;
25
+ }
26
+
27
+ /**
28
+ * 遍历 `page.config.ts` 的每个键,在 `apps/<key>` 下读取 `package.json`,
29
+ * 并为每个子应用从 4000 起顺序分配可用端口,生成 `PageConfig` 列表。
30
+ */
31
+ async function formatPageConfig(): Promise<PageConfig[]> {
32
+ const pageConfigs: PageConfig[] = [];
33
+ const appPath = path.join(process.cwd(), 'apps');
34
+ let basePort = 4000;
35
+ for (const [key, value] of Object.entries(pageConfig)) {
36
+ const appPackagePath = path.join(appPath, key, 'package.json');
37
+ try {
38
+ const appPackage = await fs.promises.readFile(appPackagePath, 'utf8');
39
+ const appPackageJson = JSON.parse(appPackage);
40
+ const port = await portfinder.getPortPromise({ port: basePort });
41
+ basePort = port + 1;
42
+ const pageConfig: PageConfig = {
43
+ key,
44
+ name: appPackageJson.name,
45
+ fullPath: path.join(appPath, key),
46
+ prefixPath: `/${key}`,
47
+ port,
48
+ };
49
+ pageConfigs.push(pageConfig);
50
+ } catch (error) {
51
+ console.error(`[${key}] 读取 package.json 失败: ${(error as Error).message}`);
52
+ continue;
53
+ }
54
+ }
55
+ return pageConfigs;
56
+ }
57
+
58
+ /**
59
+ * 在各自 `cwd` 下执行 `pnpm run dev -p <port>` 启动每个子应用。
60
+ * 监听 stdout 出现 Ready/successfully 时视为已就绪并 resolve(stderr 常含非错误日志,见下方)。
61
+ */
62
+ async function runApp(pageConfigs: PageConfig[]) {
63
+ const promises = pageConfigs.map(config => {
64
+ return new Promise<ChildProcessWithoutNullStreams>((resolve, reject) => {
65
+ const child = spawn('pnpm', ['run', 'dev'], {
66
+ cwd: config.fullPath,
67
+ env: { ...process.env, PORT: String(config.port) },
68
+ // 如需在终端直接看到子应用原始输出,可改为 stdio: 'inherit'(需自行调整就绪判断逻辑)
69
+ shell: process.platform === 'win32',
70
+ });
71
+ child.on('error', (err) => {
72
+ console.error(chalk.red(`[${config.key}] 子进程启动失败:`), err.message);
73
+ });
74
+ child.on('exit', (code, signal) => {
75
+ if (code !== null && code !== 0) {
76
+ console.error(chalk.red(`[${config.key}] 子进程退出 code=${code} signal=${signal}`));
77
+ }
78
+ });
79
+ child.stdout.on('data', (data) => {
80
+ const msg = data.toString();
81
+ console.log(`${chalk.green(`[${config.key}]`)} ${msg}`);
82
+ const reg = /Local: http:\/\/localhost:(\d+)/
83
+ const match = msg.match(reg);
84
+ if (match) {
85
+ const port = match[1];
86
+ config.port = parseInt(port);
87
+ setTimeout(() => {
88
+ resolve(child)
89
+ });
90
+ }
91
+ });
92
+ child.stderr.on('data', (data) => {
93
+ const msg = data.toString();
94
+ console.log(`${chalk.dim(`[${config.key}]`)} ${msg}`);
95
+ });
96
+ })
97
+ })
98
+ return Promise.all(promises).then((childs) => {
99
+ console.log(chalk.green('所有子应用已启动'))
100
+ pageConfigs.forEach(config => {
101
+ console.log(`${chalk.green(`[${config.key}]`)} 子应用已启动:${chalk.blue(`http://127.0.0.1:${config.port}`)}`)
102
+ })
103
+ return childs
104
+ })
105
+ }
106
+
107
+ /**
108
+ * 根据当前请求解析应转发到的子应用:优先用路径匹配;无 Referer 或 Referer 为根时用请求路径;
109
+ * 否则用 Referer 的路径(便于静态资源等请求仍落到同一子应用)。无匹配时回退上次目标或第一个应用。
110
+ */
111
+ function getTargetByRes(req: http.IncomingMessage, pageConfigs: PageConfig[], lastTargetPageConfig: PageConfig | null) {
112
+ const pathname = urlPathname(req.url) ?? '/';
113
+ const refererPathname = urlPathname(req.headers.referer)
114
+ const targetPathname = (
115
+ !refererPathname || refererPathname === '/'
116
+ ? pathname
117
+ : refererPathname
118
+ )
119
+ const pathConfig = getTargetByPath(targetPathname, pageConfigs);
120
+ return pathConfig ?? lastTargetPageConfig ?? pageConfigs[0]!;
121
+ }
122
+
123
+ /** 判断 `pathname` 是否属于某子应用(等于 `prefixPath` 或以其为前缀)。 */
124
+ function getTargetByPath(pathname: string, pageConfigs: PageConfig[]) {
125
+ for (const config of pageConfigs) {
126
+ if (pathname === config.prefixPath || pathname.startsWith(`${config.prefixPath}/`)) {
127
+ return config;
128
+ }
129
+ }
130
+ return null;
131
+ }
132
+
133
+ /** 从完整 URL 或相对路径中解析 pathname;无效输入返回 `null`。 */
134
+ function urlPathname(url: string | undefined) {
135
+ if (!url) return null;
136
+ try {
137
+ return new URL(url, 'http://127.0.0.1').pathname || null;
138
+ } catch {
139
+ return null;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * 转发到子应用 dev 服务器前去掉网关层前缀(如 `/app`),使子应用收到根路径下的请求。
145
+ */
146
+ function rewriteUrlForTarget(rawUrl: string | undefined, target: PageConfig): string {
147
+ if (!rawUrl) return '/';
148
+ const url = new URL(rawUrl, 'http://127.0.0.1');
149
+ url.pathname = url.pathname.replace(new RegExp(`^${target.prefixPath}`), '') || '/';
150
+ return url.toString();
151
+ }
152
+
153
+ /**
154
+ * 在可用端口(默认从 3000 起探测)上启动 HTTP 代理,支持 WebSocket;
155
+ * 根路径 `/` 返回各子应用入口链接,其余请求按路由规则转发到对应本地端口。
156
+ */
157
+ async function runProxyServer(pageConfigs: PageConfig[]) {
158
+ const proxy = httpProxy.createProxyServer({ ws: true });
159
+ proxy.on('error', (err, req, res) => {
160
+ console.error(chalk.red('[proxy error]'), err.message);
161
+ });
162
+
163
+ let lastTargetPageConfig: PageConfig | null = null;
164
+
165
+ const server = http.createServer((req, res) => {
166
+ const pathname = urlPathname(req.url) ?? '/';
167
+ if (pathname === '/' || pathname === '') {
168
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
169
+ const links = pageConfigs
170
+ .map((c) => ` <li><a href="${c.prefixPath}">${c.prefixPath}</a> (${c.key})</li>`)
171
+ .join('\n');
172
+ res.end(
173
+ `<!DOCTYPE html><html><body><h1>子应用入口</h1><ul>\n${links}\n</ul></body></html>`
174
+ );
175
+ return;
176
+ }
177
+
178
+ lastTargetPageConfig = getTargetByRes(req, pageConfigs, lastTargetPageConfig);
179
+ req.url = rewriteUrlForTarget(req.url, lastTargetPageConfig);
180
+ proxy.web(req, res, {
181
+ target: `http://127.0.0.1:${lastTargetPageConfig.port}`,
182
+ changeOrigin: true,
183
+ });
184
+ });
185
+
186
+ server.on('upgrade', (req, socket, head) => {
187
+ lastTargetPageConfig = getTargetByRes(req, pageConfigs, lastTargetPageConfig);
188
+ req.url = rewriteUrlForTarget(req.url, lastTargetPageConfig);
189
+ proxy.ws(req, socket, head, {
190
+ target: `http://127.0.0.1:${lastTargetPageConfig.port}`,
191
+ });
192
+ });
193
+ const port = await portfinder.getPortPromise({ port: 3000 });
194
+ server.listen(port, () => {
195
+ console.log(chalk.green(`[gateway] 代理服务已启动 端口:${chalk.blue(port)}`));
196
+ console.log(`>>> 统一通过代理访问: ${chalk.blue(`http://127.0.0.1:${port}`)}`);
197
+ pageConfigs.forEach(config => {
198
+ console.log(`${chalk.green(`[${config.key}]`)} 子应用访问路径:${chalk.blue(`http://127.0.0.1:${port}${config.prefixPath}`)}`)
199
+ })
200
+ });
201
+ return server;
202
+ }
203
+
204
+ /**
205
+ * 在收到 SIGINT(Ctrl+C)或 SIGTERM 时关闭代理 `server` 并终止所有子进程;
206
+ * `server.close` 若迟迟不回调,3 秒后强制 `process.exit(0)`。
207
+ */
208
+ function listenerClose(server: http.Server, childs: ChildProcessWithoutNullStreams[]) {
209
+ let cleaning = false;
210
+ const cleanup = () => {
211
+ if (cleaning) return;
212
+ cleaning = true;
213
+
214
+ server.close(() => {
215
+ childs.forEach((child) => {
216
+ if (child.killed) return;
217
+ child.kill('SIGTERM');
218
+ });
219
+ process.exit(0);
220
+ });
221
+ // 若 server.close 回调未及时触发,超时后强制退出
222
+ setTimeout(() => process.exit(0), 3000);
223
+ };
224
+
225
+ process.on('SIGINT', cleanup);
226
+ process.on('SIGTERM', cleanup);
227
+ }
228
+
229
+ /** 加载配置 → 拉起子应用 → 启动网关代理 → 注册退出清理。 */
230
+ async function main() {
231
+ const pageConfigs = await formatPageConfig();
232
+ if (pageConfigs.length === 0) {
233
+ console.error('未开启任何子应用,请检查 page.config.ts 配置');
234
+ process.exit(1);
235
+ }
236
+ const childs = await runApp(pageConfigs);
237
+ const proxyServer = await runProxyServer(pageConfigs);
238
+ listenerClose(proxyServer, childs);
239
+ }
240
+
241
+ main()
@@ -0,0 +1,86 @@
1
+ {
2
+ "$schema": "https://turbo.build/schema.json",
3
+ "globalEnv": [
4
+ "CDN_PUBLIC_PATH",
5
+ "BRANCH_OR_TAG",
6
+ "VERSION",
7
+ "SENTRY_AUTH_TOKEN"
8
+ ],
9
+ "envMode": "loose",
10
+ "tasks": {
11
+ "build": {
12
+ "dependsOn": ["^build"],
13
+ "inputs": [
14
+ "$TURBO_DEFAULT$",
15
+ "!.umi/**",
16
+ "!.turbo/**",
17
+ "!src/.umi/**",
18
+ "!src/.umi-production/**",
19
+ "!src/.umi-test/**",
20
+ ".env",
21
+ ".env.local",
22
+ ".env.production",
23
+ ".env.production.local"
24
+ ],
25
+ "outputs": ["dist/**"]
26
+ },
27
+ "build:development": {
28
+ "dependsOn": ["^build:development"],
29
+ "inputs": [
30
+ "$TURBO_DEFAULT$",
31
+ "!.umi/**",
32
+ "!.turbo/**",
33
+ "!src/.umi/**",
34
+ "!src/.umi-production/**",
35
+ "!src/.umi-test/**",
36
+ ".env",
37
+ ".env.local",
38
+ ".env.development",
39
+ ".env.development.local"
40
+ ],
41
+ "outputs": ["dist/**"]
42
+ },
43
+ "build:testing": {
44
+ "dependsOn": ["^build:testing"],
45
+ "inputs": [
46
+ "$TURBO_DEFAULT$",
47
+ "!.umi/**",
48
+ "!.turbo/**",
49
+ "!src/.umi/**",
50
+ "!src/.umi-production/**",
51
+ "!src/.umi-test/**",
52
+ ".env",
53
+ ".env.local",
54
+ ".env.testing",
55
+ ".env.testing.local"
56
+ ],
57
+ "outputs": ["dist/**"]
58
+ },
59
+ "build:production": {
60
+ "dependsOn": ["^build:production"],
61
+ "inputs": [
62
+ "$TURBO_DEFAULT$",
63
+ "!.umi/**",
64
+ "!.turbo/**",
65
+ "!src/.umi/**",
66
+ "!src/.umi-production/**",
67
+ "!src/.umi-test/**",
68
+ ".env",
69
+ ".env.local",
70
+ ".env.production",
71
+ ".env.production.local"
72
+ ],
73
+ "outputs": ["dist/**"]
74
+ },
75
+ "dev": {
76
+ "cache": false,
77
+ "persistent": true
78
+ },
79
+ "lint": {
80
+ "outputs": []
81
+ },
82
+ "lint:fix": {
83
+ "outputs": []
84
+ }
85
+ }
86
+ }
@@ -25,6 +25,8 @@ const config: ReturnType<typeof defineConfig> = {
25
25
  logo: '',
26
26
  apiBaseUrl: '',
27
27
  defaultPath: '',
28
+ // 布局模式, 默认 'side',可选 'mix'
29
+ // layoutMode: 'mix',
28
30
  // 免认证路由(跳过 SSO 登录),支持 /* 前缀匹配
29
31
  // noAuthRouteList: ['/*'],
30
32
  // 免权限校验路由(跳过菜单权限检查),与 mock pages 中 accessControlEnabled 示例一致时可按需配置
@@ -0,0 +1,166 @@
1
+ # 头部一级导航布局(layoutMode: mix)
2
+
3
+ > 创建时间:2026-06-15
4
+
5
+ ## 功能概述
6
+
7
+ 为主布局新增可配置的 **mix 布局模式**:将菜单树的**一级目录**渲染为头部 Tab,对应一级目录下的**子级目录**渲染在侧边栏。切换头部 Tab 时侧边栏跟随变化为对应一级目录的子树。通过 `window.__MICO_CONFIG__.layoutMode` 开关控制,默认 `side`(经典左侧完整目录树),不配置时行为与旧版完全一致。
8
+
9
+ ## 技术方案
10
+
11
+ ### 技术栈
12
+
13
+ - 框架:React 18 + @umijs/max
14
+ - UI 组件:@mico-platform/ui(`Tabs` 一级导航)
15
+ - 状态管理:完全由路由 `location.pathname` 驱动,**无新增全局状态**
16
+
17
+ ### 核心实现
18
+
19
+ 1. **配置开关**:`isMixLayout()` 读取 `window.__MICO_CONFIG__.layoutMode`,仅 `'mix'` 时启用新布局。
20
+ 2. **数据共用**:抽取 `useFilteredMenuItems()`,统一「权限过滤 + 解析」后的菜单树,供头部一级导航与侧边栏共用,避免重复解析。
21
+ 3. **激活一级推导**:`getActiveTopKey(items, pathname)` 复用 `findMenuKeyByPath` 的祖先链 `openKeys[0]` 推导当前激活的一级 key;未命中(403/404、首页、仅在 PAGES 不在 MENUS 的页面)返回 `undefined`,不高亮、不渲染子树。
22
+ 4. **头部一级导航**:mix 模式下渲染顶层菜单项为 `Tabs`,高亮跟随路由;点击 Tab 经 `findFirstLeaf` 取该一级下第一个可跳转叶子——`page` 走 `history.push`,`link` 走 `window.open`。
23
+ 5. **侧边栏子树**:mix 模式下侧边栏只渲染激活一级的 `children`;该一级**无子目录时隐藏侧边栏**(`--sider-width` 置 0,内容区占满)。
24
+
25
+ ### 数据流
26
+
27
+ ```
28
+ location.pathname
29
+
30
+ ├── 头部:getActiveTopKey(全树, pathname) → 高亮对应一级 Tab
31
+ │ 点击 Tab → findFirstLeaf(一级) → page: history.push / link: window.open
32
+
33
+ └── 侧边栏:getActiveTopKey(全树, pathname) → 取该一级 children 渲染
34
+ children 为空 → hideSider(--sider-width: 0px,不渲染 Sider)
35
+
36
+ 切 Tab = 路由跳转 → pathname 变化 → 头部高亮与侧边栏子树同步刷新
37
+ ```
38
+
39
+ ### 兼容策略
40
+
41
+ ```
42
+ isMixLayout() === false(默认 / layoutMode 未配置 / = 'side')
43
+ ├── 头部:不渲染一级导航(Logo 保持 margin-right:auto 原布局)
44
+ └── 侧边栏:渲染完整菜单树,hideSider 恒为 false ← 与旧版完全一致
45
+ ```
46
+
47
+ ## 文件清单
48
+
49
+ ### 新增文件
50
+
51
+ | 文件路径 | 说明 |
52
+ | --- | --- |
53
+ | `src/hooks/useFilteredMenuItems.ts` | 权限过滤 + 解析后的菜单树 Hook,头部与侧边栏共用 |
54
+
55
+ ### 修改文件
56
+
57
+ | 文件路径 | 修改内容 |
58
+ | --- | --- |
59
+ | `src/common/menu/types.ts` | `__MICO_CONFIG__` 新增 `layoutMode?: 'side' \| 'mix'` |
60
+ | `src/constants/index.ts` | 新增 `TLayoutMode`、`getLayoutMode()`、`isMixLayout()` |
61
+ | `src/common/menu/parser.ts` | 新增 `getActiveTopKey()`、`findFirstLeaf()` |
62
+ | `src/layouts/components/header/index.tsx` | mix 模式渲染一级 `Tabs`,按叶子类型分流跳转;mix 时根节点加 `layout-header--mix` 类 |
63
+ | `src/layouts/components/header/index.less` | 一级导航 Tab 样式(对齐设计稿);mix 模式 Logo 列宽对齐侧边栏 |
64
+ | `src/layouts/components/menu/index.tsx` | 改用 `useFilteredMenuItems()`;mix 模式渲染激活一级子树;无子目录时隐藏 Sider |
65
+ | `config/config.dev.ts` | `__MICO_CONFIG__` 增加 `layoutMode` 注释示例 |
66
+
67
+ ## API / 组件接口
68
+
69
+ ### 配置项
70
+
71
+ ```typescript
72
+ interface Window {
73
+ __MICO_CONFIG__?: {
74
+ /**
75
+ * 布局模式
76
+ * - 'side'(默认):经典左侧完整目录树
77
+ * - 'mix':一级目录在头部 Tab,子级目录在侧边栏
78
+ */
79
+ layoutMode?: 'side' | 'mix';
80
+ };
81
+ }
82
+ ```
83
+
84
+ ### 常量函数(`constants/index.ts`)
85
+
86
+ ```typescript
87
+ type TLayoutMode = 'side' | 'mix';
88
+
89
+ /** 读取布局模式,未配置默认 'side' */
90
+ function getLayoutMode(): TLayoutMode;
91
+
92
+ /** 是否为「头部一级 + 侧边二级」混合布局 */
93
+ function isMixLayout(): boolean;
94
+ ```
95
+
96
+ ### 菜单解析函数(`common/menu/parser.ts`)
97
+
98
+ ```typescript
99
+ /** 根据当前路径推导激活的一级菜单 key;未命中返回 undefined */
100
+ function getActiveTopKey(
101
+ items: ParsedMenuItem[],
102
+ pathname: string,
103
+ ): string | undefined;
104
+
105
+ /** 查找一级菜单项下第一个可跳转的叶子节点(DFS,跳过 group 容器) */
106
+ function findFirstLeaf(item: ParsedMenuItem): ParsedMenuItem | undefined;
107
+ ```
108
+
109
+ ### 共用 Hook(`hooks/useFilteredMenuItems.ts`)
110
+
111
+ ```typescript
112
+ /** 权限过滤 + 解析后的菜单树,供侧边栏与头部一级导航共用 */
113
+ function useFilteredMenuItems(): ParsedMenuItem[];
114
+ ```
115
+
116
+ ## 使用示例
117
+
118
+ 中台注入的 `window.__MICO_CONFIG__` 中开启 mix 布局:
119
+
120
+ ```html
121
+ <script>
122
+ window.__MICO_CONFIG__ = {
123
+ appName: '示例平台',
124
+ layoutMode: 'mix', // 不配置或 'side' 即经典左侧目录树
125
+ };
126
+ </script>
127
+ ```
128
+
129
+ 或在本地开发 `config/config.dev.ts` 的 `define` 中开启:
130
+
131
+ ```typescript
132
+ 'window.__MICO_CONFIG__': {
133
+ layoutMode: 'mix',
134
+ },
135
+ ```
136
+
137
+ ## 设计决策
138
+
139
+ | 决策点 | 选择 | 理由 |
140
+ | --- | --- | --- |
141
+ | 激活态来源 | 完全由 `location.pathname` 推导 | 与 `AppTabs`、`useMenuState` 既有范式一致,零新增全局状态,路由天然驱动头部与侧栏同步 |
142
+ | 数据共用 | 抽取 `useFilteredMenuItems()` | 头部与侧栏共用同一份过滤解析结果,避免重复解析与权限逻辑漂移 |
143
+ | 一级导航组件 | `Tabs`(line 型) | 与设计稿一致,自动继承主题/国际化;与内容区多页签 `AppTabs` 功能不冲突,二者并存 |
144
+ | Tab 点击跳转 | `findFirstLeaf` 按类型分流 | `page` 用 `history.push`,`link` 用 `window.open`,避免外链 URL 被推进路由 |
145
+ | 未命中路由 | `getActiveTopKey` 返回 `undefined` | 403/404、首页等不误高亮、不渲染无关子树 |
146
+ | 无子目录的一级 | 隐藏 Sider + `--sider-width: 0` | 内容区占满,避免空侧栏占位 |
147
+ | Logo 列宽对齐 | 仅 `.layout-header--mix` 生效 | mix 下使一级导航起点与内容区左边界对齐;不影响 side 模式原布局 |
148
+
149
+ ## 已知限制与待改进
150
+
151
+ - mix 模式下若某一级目录及其子级**全部为 group 容器**(无 page/link 叶子),点击该 Tab 不会触发跳转(`findFirstLeaf` 返回空)。
152
+ - Logo 列宽按侧边栏展开态(`@sidebar-width`)对齐;侧栏折叠/隐藏时不会同步调整,对齐仅在展开态成立。
153
+ - 头部一级 Tab 暂未处理 `disabled` 态(一级导航不会出现禁用项,故未实现)。
154
+
155
+ ## 注意事项
156
+
157
+ - 默认 `side` 模式与旧版行为完全一致,升级零配置。
158
+ - mix 模式不改动路由注册、权限校验、qiankun 加载逻辑,仅影响菜单的「呈现位置」。
159
+ - 头部一级导航与内容区多页签 `AppTabs` 并存、互不冲突。
160
+ - 切换头部 Tab 本质是路由跳转;若当前已在该一级下,点击同一 Tab 不会重复跳转(`Tabs` 仅在 activeTab 变化时触发 `onChange`)。
161
+
162
+ ## 相关文档
163
+
164
+ - [路由与菜单解耦](./feature-路由与菜单解耦.md) - PAGES/MENUS 数据源、`getActiveTopKey` 依赖的菜单解析
165
+ - [菜单权限控制](./feature-菜单权限控制.md) - `useFilteredMenuItems` 复用的权限过滤逻辑
166
+ - [主题色切换](./feature-主题色切换.md) - 一级导航 Tab 样式使用的主题变量
@@ -36,6 +36,7 @@ import {
36
36
  clearRedirectCount,
37
37
  ensureSsoSession,
38
38
  handleAuthFailureRedirect,
39
+ SsoNetworkError,
39
40
  } from './common/request/sso';
40
41
  import { initTheme } from './common/theme';
41
42
  import MicroAppLoader from './components/MicroAppLoader';
@@ -220,7 +221,19 @@ export async function getInitialState(): Promise<{
220
221
 
221
222
  // 非免认证路由:走 SSO 流程
222
223
  if (!skipAuth) {
223
- await ensureSsoSession();
224
+ try {
225
+ await ensureSsoSession();
226
+ } catch (error) {
227
+ // 仅 ticket 换 token 阶段的网络异常会抛出 SsoNetworkError,
228
+ // 在此显式拿到信号并触发带网络异常文案的兜底弹窗。
229
+ if (error instanceof SsoNetworkError) {
230
+ handleAuthFailureRedirect({ isNetworkError: true });
231
+ return {
232
+ fetchUserInfo: fetchUserInfoFn,
233
+ };
234
+ }
235
+ throw error;
236
+ }
224
237
  }
225
238
 
226
239
  // 仅在「需认证」路径且本地已有 token 时拉取用户信息(登录态刷新等)。
@@ -239,7 +252,12 @@ export async function getInitialState(): Promise<{
239
252
 
240
253
  // 非免认证路由且没有 token,跳转到 SSO 登录
241
254
  if (!skipAuth) {
242
- handleAuthFailureRedirect();
255
+ const redirected = handleAuthFailureRedirect();
256
+ if (redirected) {
257
+ // 阻断渲染,等待浏览器导航卸载页面,
258
+ // 避免页面组件抢先发起被 401 的请求(如 Home 的业务系统列表)
259
+ await new Promise<never>(() => {});
260
+ }
243
261
  // 返回空状态,页面会被重定向
244
262
  return {
245
263
  fetchUserInfo: fetchUserInfoFn,
@@ -440,6 +440,42 @@ export const findRouteByPath = (
440
440
  return exact || bestWildcard?.route;
441
441
  };
442
442
 
443
+ /**
444
+ * 查找一级菜单项下第一个可跳转的叶子节点
445
+ * 深度优先,跳过 group 容器,返回第一个带 path 的 page/link 节点
446
+ */
447
+ export const findFirstLeaf = (
448
+ item: ParsedMenuItem,
449
+ ): ParsedMenuItem | undefined => {
450
+ if (item.type !== 'group' && item.path) {
451
+ return item;
452
+ }
453
+ if (item.children && item.children.length > 0) {
454
+ for (const child of item.children) {
455
+ const found = findFirstLeaf(child);
456
+ if (found) return found;
457
+ }
458
+ }
459
+ return undefined;
460
+ };
461
+
462
+ /**
463
+ * 根据当前路径推导激活的一级菜单 key
464
+ * - 命中某个菜单项时,取其祖先链顶端(openKeys[0]);自身即一级时取自身 key
465
+ * - 未命中时返回 undefined,不高亮任何一级(如 403/404、首页、仅在 PAGES 不在 MENUS 的页面)
466
+ */
467
+ export const getActiveTopKey = (
468
+ items: ParsedMenuItem[],
469
+ pathname: string,
470
+ ): string | undefined => {
471
+ const result = findMenuKeyByPath(items, pathname);
472
+ if (result) {
473
+ return result.openKeys[0] ?? result.key;
474
+ }
475
+ // 未命中菜单树(如 403/404、首页、仅在 PAGES 不在 MENUS 的页面):不高亮任何一级
476
+ return undefined;
477
+ };
478
+
443
479
  /**
444
480
  * 根据路径查找对应的菜单项 key
445
481
  */
@@ -198,6 +198,12 @@ declare global {
198
198
  >;
199
199
  /** 默认重定向路径,访问 "/" 时自动跳转到此路径 */
200
200
  defaultPath?: string;
201
+ /**
202
+ * 布局模式
203
+ * - 'side'(默认):经典左侧完整目录树
204
+ * - 'mix':一级目录显示在头部 Tab,对应一级下的子级目录显示在侧边栏
205
+ */
206
+ layoutMode?: 'side' | 'mix';
201
207
  /**
202
208
  * 免认证路由列表(跳过 SSO 登录)
203
209
  * 支持精确匹配和前缀匹配(以 /* 结尾)
@@ -36,7 +36,7 @@ import {
36
36
  } from './interceptors';
37
37
 
38
38
  // SSO 相关
39
- import { handleAuthFailureRedirect } from './sso';
39
+ import { handleAuthFailureRedirect, isRedirecting } from './sso';
40
40
 
41
41
  // Token 刷新相关
42
42
  import {
@@ -74,6 +74,12 @@ export const request = async <T = unknown>(
74
74
  url: string,
75
75
  options?: UnifiedRequestOptions,
76
76
  ): Promise<T> => {
77
+ // 正在执行 SSO 硬跳转时,挂起任何在途请求,
78
+ // 避免在浏览器导航卸载前发出被 401 的请求,进而触发重复 toast 与二次跳转
79
+ if (isRedirecting()) {
80
+ return new Promise<T>(() => {});
81
+ }
82
+
77
83
  const resolvedUrl = resolveRequestUrl(url, options);
78
84
 
79
85
  const ctx: RequestContext = {
@@ -169,7 +175,7 @@ export const registerRequestInterceptor = addRequestInterceptor;
169
175
  export const registerResponseInterceptor = addResponseInterceptor;
170
176
 
171
177
  // 导出认证相关方法
172
- export { handleAuthFailureRedirect };
178
+ export { handleAuthFailureRedirect, SsoNetworkError } from './sso';
173
179
 
174
180
  // 导出类型
175
181
  export type {