@defuy/micro-cli 1.0.0
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 +78 -0
- package/bin/micro.js +48 -0
- package/lib/build.js +87 -0
- package/lib/config.js +23 -0
- package/lib/create.js +371 -0
- package/lib/deploy.js +252 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# @micro/cli
|
|
2
|
+
|
|
3
|
+
微前端脚手架工具,用于快速创建、打包和部署微前端子应用。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @micro/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 使用
|
|
12
|
+
|
|
13
|
+
### 创建功能包
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
mf c <name>
|
|
17
|
+
mf create <name>
|
|
18
|
+
|
|
19
|
+
# 示例
|
|
20
|
+
mf c dashboard
|
|
21
|
+
|
|
22
|
+
# 跳过交互确认
|
|
23
|
+
mf c dashboard -y
|
|
24
|
+
|
|
25
|
+
# 指定描述
|
|
26
|
+
mf c dashboard -y -d "仪表盘模块"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 打包
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# 打包所有子应用
|
|
33
|
+
mf b
|
|
34
|
+
|
|
35
|
+
# 打包指定子应用
|
|
36
|
+
mf b vue
|
|
37
|
+
mf b app-vue
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 部署
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# 部署所有已打包的子应用
|
|
44
|
+
mf d
|
|
45
|
+
|
|
46
|
+
# 部署指定子应用
|
|
47
|
+
mf d vue
|
|
48
|
+
|
|
49
|
+
# 同时部署基座
|
|
50
|
+
mf d --host
|
|
51
|
+
|
|
52
|
+
# 只部署配置文件
|
|
53
|
+
mf d --config-only
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## 命令说明
|
|
57
|
+
|
|
58
|
+
| 命令 | 别名 | 说明 |
|
|
59
|
+
|------|------|------|
|
|
60
|
+
| `mf c <name>` | `create` | 创建新的功能包 |
|
|
61
|
+
| `mf b [app]` | `build` | 打包功能包 |
|
|
62
|
+
| `mf d [app]` | `deploy` | 部署到服务器 |
|
|
63
|
+
|
|
64
|
+
## 选项
|
|
65
|
+
|
|
66
|
+
### create 命令
|
|
67
|
+
|
|
68
|
+
- `-y, --yes`: 跳过交互确认,使用默认值
|
|
69
|
+
- `-d, --desc <description>`: 指定功能包描述
|
|
70
|
+
|
|
71
|
+
### deploy 命令
|
|
72
|
+
|
|
73
|
+
- `--host`: 同时部署基座
|
|
74
|
+
- `--config-only`: 只部署配置文件
|
|
75
|
+
|
|
76
|
+
## 更多信息
|
|
77
|
+
|
|
78
|
+
详细文档请查看项目主 README。
|
package/bin/micro.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { createApp } from '../lib/create.js';
|
|
6
|
+
import { deploy } from '../lib/deploy.js';
|
|
7
|
+
import { build } from '../lib/build.js';
|
|
8
|
+
|
|
9
|
+
const program = new Command();
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.name('mf')
|
|
13
|
+
.description('微前端脚手架工具')
|
|
14
|
+
.version('1.0.0');
|
|
15
|
+
|
|
16
|
+
// 创建功能包
|
|
17
|
+
program
|
|
18
|
+
.command('c <name>')
|
|
19
|
+
.alias('create')
|
|
20
|
+
.description('创建新的功能包')
|
|
21
|
+
.option('-y, --yes', '跳过确认,使用默认值')
|
|
22
|
+
.option('-d, --desc <description>', '功能包描述')
|
|
23
|
+
.action(async (name, options) => {
|
|
24
|
+
console.log(chalk.cyan(`\n🚀 创建功能包: ${name}\n`));
|
|
25
|
+
await createApp(name, options);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// 打包
|
|
29
|
+
program
|
|
30
|
+
.command('b [app]')
|
|
31
|
+
.alias('build')
|
|
32
|
+
.description('打包功能包')
|
|
33
|
+
.action(async (app) => {
|
|
34
|
+
await build(app);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// 部署
|
|
38
|
+
program
|
|
39
|
+
.command('d [app]')
|
|
40
|
+
.alias('deploy')
|
|
41
|
+
.description('部署到服务器')
|
|
42
|
+
.option('--host', '同时部署基座')
|
|
43
|
+
.option('--config-only', '只部署配置文件')
|
|
44
|
+
.action(async (app, options) => {
|
|
45
|
+
await deploy(app, options);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
program.parse();
|
package/lib/build.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
|
|
10
|
+
// 后处理:修复全局变量引用
|
|
11
|
+
function fixGlobals(distDir) {
|
|
12
|
+
const jsDir = path.join(distDir, 'js');
|
|
13
|
+
if (!fs.existsSync(jsDir)) return;
|
|
14
|
+
|
|
15
|
+
const files = fs.readdirSync(jsDir).filter(f => f.endsWith('.js'));
|
|
16
|
+
|
|
17
|
+
for (const file of files) {
|
|
18
|
+
const filePath = path.join(jsDir, file);
|
|
19
|
+
let content = fs.readFileSync(filePath, 'utf-8');
|
|
20
|
+
|
|
21
|
+
// 替换 IIFE 末尾的全局变量引用
|
|
22
|
+
// (function(e,a){...})(vue,vueRouter) -> (function(e,a){...})(window.__SHARED_LIBS__.vue, window.__SHARED_LIBS__["vue-router"])
|
|
23
|
+
content = content.replace(
|
|
24
|
+
/\}\)\(vue,\s*vueRouter\);?\s*$/,
|
|
25
|
+
'})(window.__SHARED_LIBS__.vue, window.__SHARED_LIBS__["vue-router"]);'
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
fs.writeFileSync(filePath, content);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function build(appName) {
|
|
33
|
+
const packagesDir = path.resolve(__dirname, '../../');
|
|
34
|
+
const distDir = path.resolve(packagesDir, '../dist');
|
|
35
|
+
|
|
36
|
+
// 获取要打包的应用列表
|
|
37
|
+
let apps = [];
|
|
38
|
+
|
|
39
|
+
if (appName) {
|
|
40
|
+
const appId = appName.startsWith('app-') ? appName : `app-${appName}`;
|
|
41
|
+
const appDir = path.join(packagesDir, appId);
|
|
42
|
+
|
|
43
|
+
if (!fs.existsSync(appDir)) {
|
|
44
|
+
console.log(chalk.red(`❌ 功能包 ${appId} 不存在`));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
apps = [appId];
|
|
48
|
+
} else {
|
|
49
|
+
// 获取所有功能包
|
|
50
|
+
const dirs = fs.readdirSync(packagesDir);
|
|
51
|
+
apps = dirs.filter(dir => {
|
|
52
|
+
const pkgPath = path.join(packagesDir, dir, 'package.json');
|
|
53
|
+
return dir.startsWith('app-') && fs.existsSync(pkgPath);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (apps.length === 0) {
|
|
58
|
+
console.log(chalk.yellow('没有找到功能包'));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log(chalk.cyan(`\n📦 准备打包 ${apps.length} 个功能包\n`));
|
|
63
|
+
|
|
64
|
+
for (const app of apps) {
|
|
65
|
+
const spinner = ora(`打包 ${app}...`).start();
|
|
66
|
+
const appDir = path.join(packagesDir, app);
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
execSync('pnpm build', {
|
|
70
|
+
cwd: appDir,
|
|
71
|
+
stdio: 'pipe'
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// 后处理:修复全局变量引用
|
|
75
|
+
const appDistDir = path.join(distDir, app);
|
|
76
|
+
fixGlobals(appDistDir);
|
|
77
|
+
|
|
78
|
+
spinner.succeed(chalk.green(`${app} 打包成功`));
|
|
79
|
+
} catch (error) {
|
|
80
|
+
spinner.fail(chalk.red(`${app} 打包失败`));
|
|
81
|
+
console.error(error.message);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log(chalk.cyan('\n✅ 打包完成!'));
|
|
86
|
+
console.log(chalk.gray(`输出目录: dist/`));
|
|
87
|
+
}
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// 服务器配置
|
|
2
|
+
export const SERVER_CONFIG = {
|
|
3
|
+
host: 'micro.defuy.cn',
|
|
4
|
+
port: 22,
|
|
5
|
+
username: 'root',
|
|
6
|
+
// 私钥路径(相对于 cli 目录)
|
|
7
|
+
privateKeyPath: '../.ssh_key',
|
|
8
|
+
// 远程部署目录
|
|
9
|
+
remotePath: '/www/wwwroot/micro-apps/dist'
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// 端口分配(新功能包自动递增)
|
|
13
|
+
export const PORT_MAP = {
|
|
14
|
+
'app-vue': 3001,
|
|
15
|
+
'app-login': 3002
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// 获取下一个可用端口
|
|
19
|
+
export function getNextPort() {
|
|
20
|
+
const usedPorts = Object.values(PORT_MAP);
|
|
21
|
+
const maxPort = Math.max(...usedPorts, 3000);
|
|
22
|
+
return maxPort + 1;
|
|
23
|
+
}
|
package/lib/create.js
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import inquirer from 'inquirer';
|
|
7
|
+
import { getNextPort, PORT_MAP } from './config.js';
|
|
8
|
+
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
|
|
11
|
+
export async function createApp(name, options = {}) {
|
|
12
|
+
// 确保名称格式正确
|
|
13
|
+
const appId = name.startsWith('app-') ? name : `app-${name}`;
|
|
14
|
+
const appName = appId.replace('app-', '');
|
|
15
|
+
|
|
16
|
+
// 获取 packages 目录
|
|
17
|
+
const packagesDir = path.resolve(__dirname, '../../');
|
|
18
|
+
const appDir = path.join(packagesDir, appId);
|
|
19
|
+
|
|
20
|
+
// 检查是否已存在
|
|
21
|
+
if (fs.existsSync(appDir)) {
|
|
22
|
+
console.log(chalk.red(`❌ 功能包 ${appId} 已存在`));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let answers;
|
|
27
|
+
|
|
28
|
+
// 如果使用 -y 选项,跳过交互
|
|
29
|
+
if (options.yes) {
|
|
30
|
+
answers = {
|
|
31
|
+
description: options.desc || `${appName} 功能模块`,
|
|
32
|
+
confirm: true
|
|
33
|
+
};
|
|
34
|
+
console.log(chalk.gray(`功能包描述: ${answers.description}`));
|
|
35
|
+
} else {
|
|
36
|
+
// 交互式确认
|
|
37
|
+
answers = await inquirer.prompt([
|
|
38
|
+
{
|
|
39
|
+
type: 'input',
|
|
40
|
+
name: 'description',
|
|
41
|
+
message: '功能包描述:',
|
|
42
|
+
default: options.desc || `${appName} 功能模块`
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
type: 'confirm',
|
|
46
|
+
name: 'confirm',
|
|
47
|
+
message: `确认创建 ${appId}?`,
|
|
48
|
+
default: true
|
|
49
|
+
}
|
|
50
|
+
]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!answers.confirm) {
|
|
54
|
+
console.log(chalk.yellow('已取消'));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const spinner = ora('创建功能包...').start();
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
// 分配端口
|
|
62
|
+
const port = PORT_MAP[appId] || getNextPort();
|
|
63
|
+
|
|
64
|
+
// 创建目录结构
|
|
65
|
+
fs.mkdirSync(appDir, { recursive: true });
|
|
66
|
+
fs.mkdirSync(path.join(appDir, 'src/views'), { recursive: true });
|
|
67
|
+
fs.mkdirSync(path.join(appDir, 'src/router'), { recursive: true });
|
|
68
|
+
fs.mkdirSync(path.join(appDir, 'src/components'), { recursive: true });
|
|
69
|
+
|
|
70
|
+
// 生成 index.html(开发时用)
|
|
71
|
+
const indexHtml = `<!DOCTYPE html>
|
|
72
|
+
<html lang="zh-CN">
|
|
73
|
+
<head>
|
|
74
|
+
<meta charset="UTF-8">
|
|
75
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
76
|
+
<title>${appId}</title>
|
|
77
|
+
</head>
|
|
78
|
+
<body>
|
|
79
|
+
<div id="app"></div>
|
|
80
|
+
<script type="module" src="/src/main.ts"></script>
|
|
81
|
+
</body>
|
|
82
|
+
</html>
|
|
83
|
+
`;
|
|
84
|
+
fs.writeFileSync(path.join(appDir, 'index.html'), indexHtml);
|
|
85
|
+
|
|
86
|
+
// 生成 package.json
|
|
87
|
+
const packageJson = {
|
|
88
|
+
name: appId,
|
|
89
|
+
version: '1.0.0',
|
|
90
|
+
description: answers.description,
|
|
91
|
+
type: 'module',
|
|
92
|
+
scripts: {
|
|
93
|
+
dev: 'vite',
|
|
94
|
+
build: 'vite build',
|
|
95
|
+
preview: 'vite preview'
|
|
96
|
+
},
|
|
97
|
+
dependencies: {
|
|
98
|
+
vue: '^3.4.0',
|
|
99
|
+
'vue-router': '^4.2.0'
|
|
100
|
+
},
|
|
101
|
+
devDependencies: {
|
|
102
|
+
'@vitejs/plugin-vue': '^5.0.0',
|
|
103
|
+
vite: '^5.0.0'
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
fs.writeFileSync(
|
|
107
|
+
path.join(appDir, 'package.json'),
|
|
108
|
+
JSON.stringify(packageJson, null, 2)
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
// 生成 vite.config.js
|
|
112
|
+
const viteConfig = `import { defineConfig, mergeConfig } from 'vite';
|
|
113
|
+
import vue from '@vitejs/plugin-vue';
|
|
114
|
+
import { devProxyPlugin, createMicroAppConfig, resolveAppConfig } from '../shared/dev-plugin.js';
|
|
115
|
+
|
|
116
|
+
const APP_CONFIG = resolveAppConfig(import.meta.url);
|
|
117
|
+
|
|
118
|
+
export default defineConfig(({ mode }) => {
|
|
119
|
+
const isProduction = mode === 'production';
|
|
120
|
+
|
|
121
|
+
const baseConfig = {
|
|
122
|
+
plugins: [
|
|
123
|
+
vue(),
|
|
124
|
+
!isProduction && devProxyPlugin(APP_CONFIG)
|
|
125
|
+
].filter(Boolean)
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
if (isProduction) {
|
|
129
|
+
return mergeConfig(baseConfig, createMicroAppConfig(APP_CONFIG));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return mergeConfig(baseConfig, {
|
|
133
|
+
base: '/',
|
|
134
|
+
server: {
|
|
135
|
+
port: APP_CONFIG.port,
|
|
136
|
+
host: true,
|
|
137
|
+
cors: true,
|
|
138
|
+
proxy: {
|
|
139
|
+
'^/assets/': {
|
|
140
|
+
target: 'http://micro.defuy.cn:9091',
|
|
141
|
+
changeOrigin: true
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
`;
|
|
148
|
+
fs.writeFileSync(path.join(appDir, 'vite.config.js'), viteConfig);
|
|
149
|
+
|
|
150
|
+
// 生成 src/main.ts(动态路由版本)
|
|
151
|
+
const mainTs = `import { createApp, nextTick } from 'vue';
|
|
152
|
+
import { createRouter, createWebHistory } from 'vue-router';
|
|
153
|
+
import App from './App.vue';
|
|
154
|
+
|
|
155
|
+
// 动态导入所有 views 下的组件
|
|
156
|
+
const viewModules = import.meta.glob('./views/**/*.vue');
|
|
157
|
+
|
|
158
|
+
let app = null;
|
|
159
|
+
let router = null;
|
|
160
|
+
let stopWatchingRoute = null;
|
|
161
|
+
|
|
162
|
+
const APP_ID = '${appId}';
|
|
163
|
+
const ACTIVE_RULE = '/${appName}';
|
|
164
|
+
|
|
165
|
+
// 从基座获取路由配置
|
|
166
|
+
function getRouteConfig() {
|
|
167
|
+
const appsConfig = window.__APPS_CONFIG__;
|
|
168
|
+
if (appsConfig) {
|
|
169
|
+
const appConfig = appsConfig.apps?.find(a => a.id === APP_ID);
|
|
170
|
+
if (appConfig?.children?.length) {
|
|
171
|
+
console.log('📋 从基座配置加载路由:', appConfig.children.length, '个');
|
|
172
|
+
return appConfig.children;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (window.__DEV_PAGES__) {
|
|
176
|
+
console.log('📋 使用开发模式页面列表');
|
|
177
|
+
return window.__DEV_PAGES__.map((p, i) => ({ name: p.name, path: ACTIVE_RULE + p.route, icon: 'Document', order: i + 1 }));
|
|
178
|
+
}
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 递归解析路由配置
|
|
183
|
+
function parseRoutes(configs) {
|
|
184
|
+
const routes = [];
|
|
185
|
+
for (const config of configs) {
|
|
186
|
+
let routePath = config.path || '';
|
|
187
|
+
if (routePath.startsWith(ACTIVE_RULE)) routePath = routePath.slice(ACTIVE_RULE.length) || '/';
|
|
188
|
+
const componentPath = findComponentPath(routePath);
|
|
189
|
+
const route = { path: routePath, name: config.name, meta: { title: config.name, icon: config.icon } };
|
|
190
|
+
if (componentPath && viewModules[componentPath]) {
|
|
191
|
+
route.component = viewModules[componentPath];
|
|
192
|
+
} else if (config.children?.length) {
|
|
193
|
+
route.component = { template: '<router-view />' };
|
|
194
|
+
} else {
|
|
195
|
+
route.component = { template: '<div style="padding:20px;color:#999;">页面不存在: ' + routePath + '</div>' };
|
|
196
|
+
}
|
|
197
|
+
if (config.children?.length) route.children = parseRoutes(config.children);
|
|
198
|
+
routes.push(route);
|
|
199
|
+
}
|
|
200
|
+
return routes;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function findComponentPath(routePath) {
|
|
204
|
+
const pathParts = routePath.split('/').filter(Boolean);
|
|
205
|
+
if (pathParts.length === 0) pathParts.push('home');
|
|
206
|
+
const attempts = [
|
|
207
|
+
\`./views/\${pathParts.join('/')}.vue\`,
|
|
208
|
+
\`./views/\${pathParts.join('/')}/index.vue\`,
|
|
209
|
+
\`./views/\${pathParts.map(p => p.charAt(0).toUpperCase() + p.slice(1)).join('/')}.vue\`,
|
|
210
|
+
\`./views/\${pathParts[pathParts.length - 1]}.vue\`,
|
|
211
|
+
\`./views/\${pathParts[pathParts.length - 1]}/index.vue\`
|
|
212
|
+
];
|
|
213
|
+
for (const attempt of attempts) if (viewModules[attempt]) return attempt;
|
|
214
|
+
// 模糊匹配:查找目录下的任意 .vue 文件
|
|
215
|
+
const targetDir = \`./views/\${pathParts.join('/')}/\`;
|
|
216
|
+
for (const key of Object.keys(viewModules)) {
|
|
217
|
+
if (key.startsWith(targetDir) && key.endsWith('.vue')) return key;
|
|
218
|
+
}
|
|
219
|
+
const lastPart = pathParts[pathParts.length - 1].toLowerCase();
|
|
220
|
+
for (const key of Object.keys(viewModules)) {
|
|
221
|
+
const fileName = key.split('/').pop()?.replace('.vue', '').toLowerCase();
|
|
222
|
+
if ((fileName === lastPart || fileName === 'index') && key.toLowerCase().includes(lastPart)) return key;
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function createAppRouter() {
|
|
228
|
+
const routeConfigs = getRouteConfig();
|
|
229
|
+
const routes = parseRoutes(routeConfigs);
|
|
230
|
+
if (routes.length > 0 && !routes.find(r => r.path === '/')) {
|
|
231
|
+
routes.unshift({ path: '/', redirect: routes[0].path });
|
|
232
|
+
}
|
|
233
|
+
console.log('🛣️ 动态路由:', routes);
|
|
234
|
+
return createRouter({ history: createWebHistory(ACTIVE_RULE + '/'), routes });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function mount(containerId = 'app', shadowRoot) {
|
|
238
|
+
console.log('🚀 ${appName} 子应用开始挂载');
|
|
239
|
+
await nextTick();
|
|
240
|
+
const root = shadowRoot || document;
|
|
241
|
+
const container = root.getElementById(containerId);
|
|
242
|
+
if (!container) { console.error('❌ 找不到挂载点'); return; }
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
router = createAppRouter();
|
|
246
|
+
|
|
247
|
+
// 🔑 获取初始路径:优先从基座传递的目标路径获取
|
|
248
|
+
let initialPath = '/';
|
|
249
|
+
const targetPath = window.__MICRO_APP_TARGET_PATH__;
|
|
250
|
+
if (targetPath && targetPath.startsWith(ACTIVE_RULE)) {
|
|
251
|
+
initialPath = targetPath.replace(ACTIVE_RULE, '') || '/';
|
|
252
|
+
console.log('📍 使用基座传递的目标路径:', targetPath, '-> 子应用路径:', initialPath);
|
|
253
|
+
delete window.__MICRO_APP_TARGET_PATH__;
|
|
254
|
+
} else {
|
|
255
|
+
const browserPath = window.location.pathname;
|
|
256
|
+
if (browserPath.startsWith(ACTIVE_RULE)) {
|
|
257
|
+
initialPath = browserPath.replace(ACTIVE_RULE, '') || '/';
|
|
258
|
+
console.log('📍 使用浏览器路径:', browserPath, '-> 子应用路径:', initialPath);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
await router.replace(initialPath);
|
|
262
|
+
|
|
263
|
+
app = createApp(App);
|
|
264
|
+
app.use(router);
|
|
265
|
+
app.mount(container);
|
|
266
|
+
console.log('✅ ${appName} 子应用已挂载, 初始路径:', initialPath);
|
|
267
|
+
|
|
268
|
+
const hostRouter = window.__MICRO_APP_ROUTER__;
|
|
269
|
+
if (hostRouter && router) {
|
|
270
|
+
stopWatchingRoute = hostRouter.afterEach((to) => {
|
|
271
|
+
if (to.path.startsWith(ACTIVE_RULE) && router) {
|
|
272
|
+
const subPath = to.path.replace(ACTIVE_RULE, '') || '/';
|
|
273
|
+
if (router.currentRoute.value.path !== subPath) {
|
|
274
|
+
console.log('🔄 子应用路由同步:', subPath);
|
|
275
|
+
router.replace(subPath);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
} catch (error) { console.error('❌ 挂载失败:', error); }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function unmount() {
|
|
284
|
+
if (stopWatchingRoute) { stopWatchingRoute(); stopWatchingRoute = null; }
|
|
285
|
+
if (app) { app.unmount(); app = null; router = null; console.log('✅ ${appName} 子应用已卸载'); }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
window.__MICRO_APP_${appName.toUpperCase().replace(/-/g, '_')}__ = { mount, unmount };
|
|
289
|
+
console.log('✅ ${appName} 子应用脚本已加载');
|
|
290
|
+
`;
|
|
291
|
+
fs.writeFileSync(path.join(appDir, 'src/main.ts'), mainTs);
|
|
292
|
+
|
|
293
|
+
// 生成 src/App.vue
|
|
294
|
+
const appVue = `<template>
|
|
295
|
+
<router-view />
|
|
296
|
+
</template>
|
|
297
|
+
|
|
298
|
+
<script setup>
|
|
299
|
+
</script>
|
|
300
|
+
`;
|
|
301
|
+
fs.writeFileSync(path.join(appDir, 'src/App.vue'), appVue);
|
|
302
|
+
|
|
303
|
+
// 生成 src/router/index.ts
|
|
304
|
+
const routerTs = `import { createRouter, createWebHistory } from 'vue-router';
|
|
305
|
+
import Home from '../views/Home.vue';
|
|
306
|
+
|
|
307
|
+
const routes = [
|
|
308
|
+
{
|
|
309
|
+
path: '/',
|
|
310
|
+
redirect: '/home'
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
path: '/home',
|
|
314
|
+
name: 'Home',
|
|
315
|
+
component: Home
|
|
316
|
+
}
|
|
317
|
+
];
|
|
318
|
+
|
|
319
|
+
const router = createRouter({
|
|
320
|
+
history: createWebHistory('/${appName}/'),
|
|
321
|
+
routes
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
export default router;
|
|
325
|
+
`;
|
|
326
|
+
fs.writeFileSync(path.join(appDir, 'src/router/index.ts'), routerTs);
|
|
327
|
+
|
|
328
|
+
// 生成 src/views/Home.vue
|
|
329
|
+
const homeVue = `<template>
|
|
330
|
+
<div class="home">
|
|
331
|
+
<h1>${appName} 首页</h1>
|
|
332
|
+
<p>这是 ${appId} 功能包的首页</p>
|
|
333
|
+
</div>
|
|
334
|
+
</template>
|
|
335
|
+
|
|
336
|
+
<script setup>
|
|
337
|
+
</script>
|
|
338
|
+
|
|
339
|
+
<style scoped>
|
|
340
|
+
.home {
|
|
341
|
+
padding: 20px;
|
|
342
|
+
}
|
|
343
|
+
</style>
|
|
344
|
+
`;
|
|
345
|
+
fs.writeFileSync(path.join(appDir, 'src/views/Home.vue'), homeVue);
|
|
346
|
+
|
|
347
|
+
// 生成 src/env.d.ts
|
|
348
|
+
const envDts = `/// <reference types="vite/client" />
|
|
349
|
+
|
|
350
|
+
declare module '*.vue' {
|
|
351
|
+
import type { DefineComponent } from 'vue';
|
|
352
|
+
const component: DefineComponent<{}, {}, any>;
|
|
353
|
+
export default component;
|
|
354
|
+
}
|
|
355
|
+
`;
|
|
356
|
+
fs.writeFileSync(path.join(appDir, 'src/env.d.ts'), envDts);
|
|
357
|
+
|
|
358
|
+
spinner.succeed(chalk.green(`功能包 ${appId} 创建成功!`));
|
|
359
|
+
|
|
360
|
+
console.log(chalk.cyan('\n下一步:'));
|
|
361
|
+
console.log(chalk.white(` cd packages/${appId}`));
|
|
362
|
+
console.log(chalk.white(` pnpm install`));
|
|
363
|
+
console.log(chalk.white(` pnpm dev`));
|
|
364
|
+
console.log(chalk.gray(`\n端口: ${port}`));
|
|
365
|
+
console.log(chalk.gray(`路由: /${appName}`));
|
|
366
|
+
|
|
367
|
+
} catch (error) {
|
|
368
|
+
spinner.fail(chalk.red('创建失败'));
|
|
369
|
+
console.error(error);
|
|
370
|
+
}
|
|
371
|
+
}
|
package/lib/deploy.js
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
import { NodeSSH } from 'node-ssh';
|
|
8
|
+
import { SERVER_CONFIG } from './config.js';
|
|
9
|
+
|
|
10
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
|
|
12
|
+
// 获取项目路径
|
|
13
|
+
function getPaths() {
|
|
14
|
+
const cliDir = path.resolve(__dirname, '..');
|
|
15
|
+
const packagesDir = path.resolve(cliDir, '..');
|
|
16
|
+
const microAppsDir = path.resolve(packagesDir, '..');
|
|
17
|
+
const distDir = path.resolve(microAppsDir, 'dist');
|
|
18
|
+
const hostDir = path.resolve(microAppsDir, '..', 'host');
|
|
19
|
+
|
|
20
|
+
return { cliDir, packagesDir, microAppsDir, distDir, hostDir };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 连接服务器
|
|
24
|
+
async function connectSSH() {
|
|
25
|
+
const ssh = new NodeSSH();
|
|
26
|
+
const { cliDir } = getPaths();
|
|
27
|
+
const privateKeyPath = path.resolve(cliDir, '.ssh_key');
|
|
28
|
+
|
|
29
|
+
if (!fs.existsSync(privateKeyPath)) {
|
|
30
|
+
throw new Error(`私钥文件不存在: ${privateKeyPath}\n请在 cli 目录下创建 .ssh_key 文件`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
await ssh.connect({
|
|
34
|
+
host: SERVER_CONFIG.host,
|
|
35
|
+
port: SERVER_CONFIG.port,
|
|
36
|
+
username: SERVER_CONFIG.username,
|
|
37
|
+
privateKey: fs.readFileSync(privateKeyPath, 'utf8')
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return ssh;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 上传目录
|
|
44
|
+
async function uploadDirectory(ssh, localDir, remoteDir) {
|
|
45
|
+
// 确保远程目录存在
|
|
46
|
+
await ssh.execCommand(`mkdir -p ${remoteDir}`);
|
|
47
|
+
|
|
48
|
+
const files = fs.readdirSync(localDir);
|
|
49
|
+
|
|
50
|
+
for (const file of files) {
|
|
51
|
+
const localPath = path.join(localDir, file);
|
|
52
|
+
const remotePath = `${remoteDir}/${file}`;
|
|
53
|
+
const stat = fs.statSync(localPath);
|
|
54
|
+
|
|
55
|
+
if (stat.isDirectory()) {
|
|
56
|
+
await uploadDirectory(ssh, localPath, remotePath);
|
|
57
|
+
} else {
|
|
58
|
+
await ssh.putFile(localPath, remotePath);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 部署基座
|
|
64
|
+
async function deployHost(ssh) {
|
|
65
|
+
const spinner = ora('部署基座...').start();
|
|
66
|
+
const { hostDir } = getPaths();
|
|
67
|
+
|
|
68
|
+
// 检查基座目录
|
|
69
|
+
if (!fs.existsSync(hostDir)) {
|
|
70
|
+
spinner.fail(chalk.red('基座目录不存在'));
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
// 打包基座
|
|
76
|
+
spinner.text = '打包基座...';
|
|
77
|
+
execSync('pnpm build', { cwd: hostDir, stdio: 'pipe' });
|
|
78
|
+
|
|
79
|
+
const hostDistDir = path.join(hostDir, 'dist');
|
|
80
|
+
const remotePath = SERVER_CONFIG.remotePath;
|
|
81
|
+
|
|
82
|
+
// 备份
|
|
83
|
+
spinner.text = '备份远程文件...';
|
|
84
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
85
|
+
await ssh.execCommand(`cp -r ${remotePath} ${remotePath}_backup_${timestamp} 2>/dev/null || true`);
|
|
86
|
+
|
|
87
|
+
// 清理基座文件(保留子应用目录和 apps-config.json)
|
|
88
|
+
spinner.text = '清理旧文件...';
|
|
89
|
+
await ssh.execCommand(`find ${remotePath} -maxdepth 1 -type f ! -name 'apps-config.json' -delete 2>/dev/null || true`);
|
|
90
|
+
await ssh.execCommand(`rm -rf ${remotePath}/assets 2>/dev/null || true`);
|
|
91
|
+
|
|
92
|
+
// 上传基座文件
|
|
93
|
+
spinner.text = '上传基座文件...';
|
|
94
|
+
await uploadDirectory(ssh, hostDistDir, remotePath);
|
|
95
|
+
|
|
96
|
+
spinner.succeed(chalk.green('基座部署成功'));
|
|
97
|
+
return true;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
spinner.fail(chalk.red('基座部署失败'));
|
|
100
|
+
console.error(error.message);
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 部署功能包
|
|
106
|
+
async function deployApp(ssh, appId) {
|
|
107
|
+
const spinner = ora(`部署 ${appId}...`).start();
|
|
108
|
+
const { distDir } = getPaths();
|
|
109
|
+
|
|
110
|
+
const localAppDir = path.join(distDir, appId);
|
|
111
|
+
const remoteAppDir = `${SERVER_CONFIG.remotePath}/${appId}`;
|
|
112
|
+
|
|
113
|
+
if (!fs.existsSync(localAppDir)) {
|
|
114
|
+
spinner.warn(chalk.yellow(`${appId} 未打包,跳过`));
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
// 清理远程目录
|
|
120
|
+
await ssh.execCommand(`rm -rf ${remoteAppDir}`);
|
|
121
|
+
|
|
122
|
+
// 上传
|
|
123
|
+
await uploadDirectory(ssh, localAppDir, remoteAppDir);
|
|
124
|
+
|
|
125
|
+
spinner.succeed(chalk.green(`${appId} 部署成功`));
|
|
126
|
+
return true;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
spinner.fail(chalk.red(`${appId} 部署失败`));
|
|
129
|
+
console.error(error.message);
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 部署配置文件
|
|
135
|
+
async function deployConfig(ssh) {
|
|
136
|
+
const spinner = ora('部署配置文件...').start();
|
|
137
|
+
const { distDir } = getPaths();
|
|
138
|
+
|
|
139
|
+
const localConfigPath = path.join(distDir, 'apps-config.json');
|
|
140
|
+
const remoteConfigPath = `${SERVER_CONFIG.remotePath}/apps-config.json`;
|
|
141
|
+
|
|
142
|
+
if (!fs.existsSync(localConfigPath)) {
|
|
143
|
+
spinner.fail(chalk.red('配置文件不存在'));
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
await ssh.putFile(localConfigPath, remoteConfigPath);
|
|
149
|
+
spinner.succeed(chalk.green('配置文件部署成功'));
|
|
150
|
+
return true;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
spinner.fail(chalk.red('配置文件部署失败'));
|
|
153
|
+
console.error(error.message);
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function deploy(appName, options = {}) {
|
|
159
|
+
const { packagesDir, distDir } = getPaths();
|
|
160
|
+
|
|
161
|
+
// 只部署配置文件
|
|
162
|
+
if (options.configOnly) {
|
|
163
|
+
console.log(chalk.cyan('\n🚀 部署配置文件...\n'));
|
|
164
|
+
|
|
165
|
+
const connectSpinner = ora('连接服务器...').start();
|
|
166
|
+
let ssh;
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
ssh = await connectSSH();
|
|
170
|
+
connectSpinner.succeed(chalk.green(`已连接 ${SERVER_CONFIG.host}`));
|
|
171
|
+
} catch (error) {
|
|
172
|
+
connectSpinner.fail(chalk.red('服务器连接失败'));
|
|
173
|
+
console.error(error.message);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
await deployConfig(ssh);
|
|
179
|
+
console.log(chalk.cyan('\n✅ 配置部署完成!\n'));
|
|
180
|
+
} finally {
|
|
181
|
+
ssh.dispose();
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 获取要部署的应用列表
|
|
187
|
+
let apps = [];
|
|
188
|
+
|
|
189
|
+
if (appName) {
|
|
190
|
+
const appId = appName.startsWith('app-') ? appName : `app-${appName}`;
|
|
191
|
+
apps = [appId];
|
|
192
|
+
} else {
|
|
193
|
+
// 获取 dist 目录下所有已打包的应用
|
|
194
|
+
if (fs.existsSync(distDir)) {
|
|
195
|
+
const dirs = fs.readdirSync(distDir);
|
|
196
|
+
apps = dirs.filter(dir => {
|
|
197
|
+
const dirPath = path.join(distDir, dir);
|
|
198
|
+
return dir.startsWith('app-') && fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory();
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 如果没有指定应用且没有 --host,提示用户
|
|
204
|
+
if (apps.length === 0 && !options.host) {
|
|
205
|
+
console.log(chalk.yellow('\n没有找到已打包的功能包'));
|
|
206
|
+
console.log(chalk.gray('请先运行 mf b 打包,或使用 mf d --host 部署基座\n'));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
console.log(chalk.cyan('\n🚀 开始部署...\n'));
|
|
211
|
+
|
|
212
|
+
// 连接服务器
|
|
213
|
+
const connectSpinner = ora('连接服务器...').start();
|
|
214
|
+
let ssh;
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
ssh = await connectSSH();
|
|
218
|
+
connectSpinner.succeed(chalk.green(`已连接 ${SERVER_CONFIG.host}`));
|
|
219
|
+
} catch (error) {
|
|
220
|
+
connectSpinner.fail(chalk.red('服务器连接失败'));
|
|
221
|
+
console.error(error.message);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
let success = true;
|
|
227
|
+
|
|
228
|
+
// 部署基座
|
|
229
|
+
if (options.host) {
|
|
230
|
+
const hostResult = await deployHost(ssh);
|
|
231
|
+
success = success && hostResult;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 部署功能包
|
|
235
|
+
for (const app of apps) {
|
|
236
|
+
const appResult = await deployApp(ssh, app);
|
|
237
|
+
success = success && appResult;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (success) {
|
|
241
|
+
console.log(chalk.cyan('\n✅ 部署完成!'));
|
|
242
|
+
console.log(chalk.gray(`访问地址: http://${SERVER_CONFIG.host}:9091\n`));
|
|
243
|
+
} else {
|
|
244
|
+
console.log(chalk.yellow('\n⚠️ 部分部署失败,请检查日志\n'));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
} catch (error) {
|
|
248
|
+
console.error(chalk.red('\n❌ 部署失败:'), error.message);
|
|
249
|
+
} finally {
|
|
250
|
+
ssh.dispose();
|
|
251
|
+
}
|
|
252
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@defuy/micro-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "微前端脚手架工具",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "bin/micro.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mf": "./bin/micro.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"lib"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"micro-frontend",
|
|
16
|
+
"cli",
|
|
17
|
+
"vue",
|
|
18
|
+
"vite",
|
|
19
|
+
"scaffold"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"commander": "^11.1.0",
|
|
26
|
+
"inquirer": "^9.2.12",
|
|
27
|
+
"chalk": "^5.3.0",
|
|
28
|
+
"ora": "^7.0.1",
|
|
29
|
+
"node-ssh": "^13.2.0"
|
|
30
|
+
}
|
|
31
|
+
}
|