flu-cli 2.0.3 → 2.0.4
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/CHANGELOG.md +28 -6
- package/CLI.md +1 -0
- package/README.md +139 -67
- package/config/templates.js +6 -127
- package/index.js +98 -50
- package/lib/commands/add.js +7 -7
- package/lib/commands/assets.js +183 -0
- package/lib/commands/config.js +41 -1
- package/lib/commands/newClack.js +85 -112
- package/lib/commands/snippets.js +50 -9
- package/lib/templates/templateCopier.js +14 -275
- package/lib/templates/templateManager.js +20 -163
- package/lib/utils/config.js +13 -51
- package/lib/utils/flutterHelper.js +7 -82
- package/lib/utils/i18n.js +7 -0
- package/lib/utils/templateSelectorEnquirer.js +70 -43
- package/locales/en-US.json +59 -0
- package/locales/zh-CN.json +59 -0
- package/package.json +1 -1
- package/release.sh +380 -32
- package/scripts/sync-base-to-templates.js +6 -6
- package/scripts/workspace-clone-all.sh +4 -4
- package/scripts/workspace-status-all.sh +3 -3
- package/lib/generators/project_generator.js +0 -96
- package/lib/generators/state_manager_generator.js +0 -402
- package/templates/README.md +0 -138
- package/templates/base_files/base_list_page.dart.template +0 -174
- package/templates/base_files/base_list_viewmodel.dart.template +0 -134
- package/templates/base_files/base_page.dart.template +0 -251
- package/templates/base_files/base_viewmodel.dart.template +0 -77
- package/templates/base_files/theme/status_views_theme.dart.template +0 -46
- package/templates/snippets/dart.code-snippets +0 -392
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
// 项目生成器:复制模板、处理占位、触发状态管理生成
|
|
2
|
-
import { existsSync } from 'fs';
|
|
3
|
-
import fsx from 'fs-extra';
|
|
4
|
-
import { join, dirname } from 'path';
|
|
5
|
-
import { fileURLToPath } from 'url';
|
|
6
|
-
import chalk from 'chalk';
|
|
7
|
-
import { logger } from '../utils/logger.js';
|
|
8
|
-
import { StateManagerGenerator } from './state_manager_generator.js';
|
|
9
|
-
|
|
10
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* 项目生成器
|
|
14
|
-
* 负责复制模板、渲染入口与依赖、并执行状态管理器配置
|
|
15
|
-
*/
|
|
16
|
-
export class ProjectGenerator {
|
|
17
|
-
/**
|
|
18
|
-
* 检查项目目录是否存在
|
|
19
|
-
*/
|
|
20
|
-
checkProjectExists (projectPath) {
|
|
21
|
-
return existsSync(projectPath);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* 复制内置模板到目标目录(lite/modular/clean)
|
|
26
|
-
*/
|
|
27
|
-
async copyTemplate (templateName, projectPath) {
|
|
28
|
-
const source = join(__dirname, '../../..', `template-${templateName}`);
|
|
29
|
-
if (!(await fsx.pathExists(source))) {
|
|
30
|
-
throw new Error(`内置模板不存在: ${source}`);
|
|
31
|
-
}
|
|
32
|
-
await fsx.copy(source, projectPath, { overwrite: true });
|
|
33
|
-
logger.success(`✓ 已复制模板: ${templateName}`);
|
|
34
|
-
return true;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* 根据用户选择配置状态管理器(适配器与入口)
|
|
39
|
-
*/
|
|
40
|
-
async configureStateManager (projectPath, stateManager, options = {}) {
|
|
41
|
-
const sm = new StateManagerGenerator(projectPath, stateManager, options);
|
|
42
|
-
await sm.generate();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* 处理模板占位符:将 .template 文件落地为实际文件
|
|
47
|
-
*/
|
|
48
|
-
async processTemplates (projectPath) {
|
|
49
|
-
const pubspecTpl = join(projectPath, 'pubspec.yaml.template');
|
|
50
|
-
const pubspecOut = join(projectPath, 'pubspec.yaml');
|
|
51
|
-
if (await fsx.pathExists(pubspecTpl)) {
|
|
52
|
-
const content = await fsx.readFile(pubspecTpl, 'utf8');
|
|
53
|
-
await fsx.writeFile(pubspecOut, content);
|
|
54
|
-
await fsx.remove(pubspecTpl);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const mainTpl = join(projectPath, 'lib', 'main.dart.template');
|
|
58
|
-
const mainOut = join(projectPath, 'lib', 'main.dart');
|
|
59
|
-
if (await fsx.pathExists(mainTpl)) {
|
|
60
|
-
const content = await fsx.readFile(mainTpl, 'utf8');
|
|
61
|
-
await fsx.writeFile(mainOut, content);
|
|
62
|
-
await fsx.remove(mainTpl);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const initScript = join(projectPath, 'init.sh');
|
|
66
|
-
if (await fsx.pathExists(initScript)) {
|
|
67
|
-
await fsx.remove(initScript);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const analysisTpl = join(projectPath, 'analysis_options.yaml.template');
|
|
71
|
-
const analysisOut = join(projectPath, 'analysis_options.yaml');
|
|
72
|
-
if (await fsx.pathExists(analysisTpl)) {
|
|
73
|
-
const content = await fsx.readFile(analysisTpl, 'utf8');
|
|
74
|
-
if (await fsx.pathExists(analysisOut)) {
|
|
75
|
-
await fsx.remove(analysisOut);
|
|
76
|
-
}
|
|
77
|
-
await fsx.writeFile(analysisOut, content);
|
|
78
|
-
await fsx.remove(analysisTpl);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// 统一 analysis_options.yaml 内容,避免重复或历史残留
|
|
82
|
-
const unified = `# 引入 Flutter 官方推荐的 lint 规则集,确保项目遵循社区最佳实践\ninclude: package:flutter_lints/flutter.yaml\n\nlinter:\n rules:\n # 文本与导入相关规则\n # 强制使用单引号,保持字符串风格统一,减少混用带来的视觉差异\n prefer_single_quotes: true\n # 对导入语句按字母顺序及来源分组排序,提升可读性并减少合并冲突\n directives_ordering: true\n\n # const 与不可变性相关规则\n # 鼓励使用 const 构造函数,提前暴露编译期常量,减少运行时对象创建\n prefer_const_constructors: true\n # 鼓励使用 const 字面量创建不可变集合,进一步节省内存并提升性能\n prefer_const_literals_to_create_immutables: true\n\n # 可读性与一致性相关规则\n # 要求多行参数、集合、列表等末尾加逗号,使格式化后自动换行更美观\n require_trailing_commas: true\n # 推荐将类字段声明为 final,强化不可变语义,减少意外修改\n prefer_final_fields: true\n # 推荐将局部变量声明为 final,明确变量只赋值一次,降低副作用\n prefer_final_locals: true\n # 建议使用 SizedBox 替代 Container 作为空白占位,语义更清晰且性能更好\n sized_box_for_whitespace: true\n # 避免使用不必要的 Container 嵌套,减少冗余节点,提升渲染性能\n avoid_unnecessary_containers: true\n\n # Flutter 常见建议规则\n # 在异步操作后使用 BuildContext 时,先检查 mounted,防止异步回调时组件已卸载导致异常\n use_build_context_synchronously: true\n`;
|
|
83
|
-
await fsx.writeFile(analysisOut, unified);
|
|
84
|
-
|
|
85
|
-
return true;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* 执行 flutter pub get(调用者触发,不在此直接运行)
|
|
90
|
-
*/
|
|
91
|
-
async runPubGet () {
|
|
92
|
-
logger.info(chalk.gray('提示: 请在项目目录中执行 `flutter pub get`'));
|
|
93
|
-
return true;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
}
|
|
@@ -1,402 +0,0 @@
|
|
|
1
|
-
// 统一状态管理生成器:按模板与状态管理器选择执行依赖注入、入口增强、路由增强与变体生成
|
|
2
|
-
// 原则:最小侵入、单一代码路径、可演进(便于后续扩展 riverpod 专用变体等)
|
|
3
|
-
import fsx from 'fs-extra';
|
|
4
|
-
import { join, dirname } from 'path';
|
|
5
|
-
import { fileURLToPath } from 'url';
|
|
6
|
-
import Handlebars from 'handlebars';
|
|
7
|
-
|
|
8
|
-
import { logger } from '../utils/logger.js';
|
|
9
|
-
import { detectProjectTemplate } from '../utils/project_detector.js';
|
|
10
|
-
|
|
11
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* 状态管理器生成器
|
|
15
|
-
* 功能:
|
|
16
|
-
* - 注入依赖(get/provider/flutter_riverpod)
|
|
17
|
-
* - 所有模板生成 BaseViewModel 变体(ChangeNotifier/GetX)
|
|
18
|
-
* - 所有模板按需增强入口(GetMaterialApp/ProviderScope)与路由(GetPage 列表)
|
|
19
|
-
*/
|
|
20
|
-
export class StateManagerGenerator {
|
|
21
|
-
/**
|
|
22
|
-
* 构造函数
|
|
23
|
-
* @param {string} projectPath 项目根目录
|
|
24
|
-
* @param {'default'|'provider'|'getx'|'riverpod'} stateManager 选中的状态管理器
|
|
25
|
-
* @param {{module?: string}} options 额外选项
|
|
26
|
-
*/
|
|
27
|
-
constructor(projectPath, stateManager = 'default', options = {}) {
|
|
28
|
-
this.projectPath = projectPath;
|
|
29
|
-
this.stateManager = stateManager;
|
|
30
|
-
this.module = options.module || null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* 主流程:依赖 → BaseViewModel 生成 → 入口 → App/路由增强 → 完成
|
|
35
|
-
*/
|
|
36
|
-
async generate () {
|
|
37
|
-
logger.info(`🔧 配置状态管理器: ${this.stateManager}`);
|
|
38
|
-
try {
|
|
39
|
-
await this.addDependencies();
|
|
40
|
-
|
|
41
|
-
// 所有模板都生成 BaseViewModel (统一策略)
|
|
42
|
-
await this.generateBaseViewModel();
|
|
43
|
-
|
|
44
|
-
await this.configureMain();
|
|
45
|
-
await this.configureAppWidget();
|
|
46
|
-
await this.configureRoutes();
|
|
47
|
-
|
|
48
|
-
logger.success('✅ 状态管理器配置完成');
|
|
49
|
-
} catch (error) {
|
|
50
|
-
logger.error(`❌ 状态管理器配置失败: ${error.message}`);
|
|
51
|
-
throw error;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* 注入依赖到 pubspec.yaml:
|
|
57
|
-
* - 处理模板占位 {{{state_manager_dependency}}} / {{{flu_core_dependency}}}
|
|
58
|
-
* - 兼容非模板 pubspec,通过正则插入
|
|
59
|
-
*/
|
|
60
|
-
async addDependencies () {
|
|
61
|
-
const pubspecPath = join(this.projectPath, 'pubspec.yaml');
|
|
62
|
-
|
|
63
|
-
if (!(await fsx.pathExists(pubspecPath))) {
|
|
64
|
-
throw new Error(`pubspec.yaml not found at ${pubspecPath}`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
let content = await fsx.readFile(pubspecPath, 'utf8');
|
|
68
|
-
const dependency = this.getDependency();
|
|
69
|
-
|
|
70
|
-
// 移除 flu_core_dependency 占位符
|
|
71
|
-
if (content.includes('{{{flu_core_dependency}}}')) {
|
|
72
|
-
content = content.replace(/\s*\{{{flu_core_dependency}}}\s*\n?/, '');
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (content.includes('{{{state_manager_dependency}}}')) {
|
|
76
|
-
content = content.replace('{{{state_manager_dependency}}}', dependency);
|
|
77
|
-
} else if (dependency) {
|
|
78
|
-
content = content.replace(/(dependencies:\s*\n(?:[\s\S]*?))(\n\w)/, `$1\n ${dependency}$2`);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
await fsx.writeFile(pubspecPath, content);
|
|
82
|
-
if (dependency) logger.info(` ✓ 添加依赖: ${dependency.split(':')[0]}`);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* 返回选中状态管理器的依赖字符串
|
|
87
|
-
*/
|
|
88
|
-
getDependency () {
|
|
89
|
-
const map = {
|
|
90
|
-
default: '',
|
|
91
|
-
provider: 'provider: ^6.1.1',
|
|
92
|
-
getx: 'get: ^4.6.6',
|
|
93
|
-
riverpod: 'flutter_riverpod: ^2.4.9'
|
|
94
|
-
};
|
|
95
|
-
return map[this.stateManager] || '';
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* 为所有模板生成 BaseViewModel 变体并重导出统一入口
|
|
100
|
-
*/
|
|
101
|
-
async generateBaseViewModel () {
|
|
102
|
-
const { detectProjectTemplate } = await import('../utils/project_detector.js');
|
|
103
|
-
const tpl = detectProjectTemplate(this.projectPath);
|
|
104
|
-
|
|
105
|
-
// 根据模板类型确定目标路径
|
|
106
|
-
let baseDir;
|
|
107
|
-
if (tpl === 'lite') {
|
|
108
|
-
baseDir = join(this.projectPath, 'lib', 'base');
|
|
109
|
-
} else if (tpl === 'modular') {
|
|
110
|
-
baseDir = join(this.projectPath, 'lib', 'core', 'base');
|
|
111
|
-
} else if (tpl === 'clean') {
|
|
112
|
-
baseDir = join(this.projectPath, 'lib', 'core', 'base');
|
|
113
|
-
} else {
|
|
114
|
-
return; // 未知模板,跳过
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
await fsx.ensureDir(baseDir);
|
|
118
|
-
const target = join(baseDir, 'base_viewmodel.dart');
|
|
119
|
-
|
|
120
|
-
const content = this.buildBaseViewModelContent();
|
|
121
|
-
await fsx.writeFile(target, content);
|
|
122
|
-
|
|
123
|
-
const relPath = target.replace(this.projectPath + '/', '');
|
|
124
|
-
logger.info(` ✓ 生成 BaseViewModel: ${relPath}`);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* 构建 BaseViewModel 变体源码:
|
|
129
|
-
* - getx:GetxController + 自管理监听列表(兼容 BasePage 的 addListener/removeListener)
|
|
130
|
-
* - default/provider:ChangeNotifier 变体
|
|
131
|
-
*/
|
|
132
|
-
buildBaseViewModelContent () {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const commonInfo = `
|
|
136
|
-
ViewState _state = ViewState.idle;
|
|
137
|
-
String? _errorMessage;
|
|
138
|
-
bool _isRefreshing = false;
|
|
139
|
-
|
|
140
|
-
// ==================== Getters ====================
|
|
141
|
-
// 当前视图状态
|
|
142
|
-
ViewState get state => _state;
|
|
143
|
-
// 错误信息
|
|
144
|
-
String? get errorMessage => _errorMessage;
|
|
145
|
-
// 是否正在加载
|
|
146
|
-
bool get isLoading => _state == ViewState.loading;
|
|
147
|
-
// 是否加载失败
|
|
148
|
-
bool get isError => _state == ViewState.error;
|
|
149
|
-
// 是否加载成功
|
|
150
|
-
bool get isSuccess => _state == ViewState.success;
|
|
151
|
-
// 是否空闲状态
|
|
152
|
-
bool get isIdle => _state == ViewState.idle;
|
|
153
|
-
// 是否正在刷新
|
|
154
|
-
bool get isRefreshing => _isRefreshing;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
// ==================== 状态设置 ====================
|
|
158
|
-
|
|
159
|
-
/// 设置视图状态
|
|
160
|
-
///
|
|
161
|
-
/// [state] 新的状态
|
|
162
|
-
/// [error] 错误信息(仅在 error 状态时有效)
|
|
163
|
-
void setState(ViewState state, {String? error}) {
|
|
164
|
-
_state = state;
|
|
165
|
-
_errorMessage = error;
|
|
166
|
-
notifyListeners();
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/// 设置刷新状态
|
|
170
|
-
void setRefreshing(bool refreshing) {
|
|
171
|
-
_isRefreshing = refreshing;
|
|
172
|
-
notifyListeners();
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
`;
|
|
177
|
-
if (this.stateManager === 'getx') {
|
|
178
|
-
return `import 'package:get/get.dart';
|
|
179
|
-
enum ViewState { idle, loading, success, error }
|
|
180
|
-
class BaseViewModel extends GetxController {
|
|
181
|
-
${commonInfo}
|
|
182
|
-
|
|
183
|
-
/// 刷新数据(通知所有监听器)
|
|
184
|
-
void notifyListeners() {
|
|
185
|
-
notifyDataChange();
|
|
186
|
-
}
|
|
187
|
-
/// 通知数据变化(仅用于局部刷新)
|
|
188
|
-
void notifyDataChange() {
|
|
189
|
-
update();
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// ==================== 生命周期 ====================
|
|
193
|
-
/// 初始化钩子(函数级注释)
|
|
194
|
-
@override
|
|
195
|
-
Future<void> onInit() async {
|
|
196
|
-
super.onInit();
|
|
197
|
-
// 子类可以重写此方法进行初始化
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/// 刷新数据(钩子)
|
|
201
|
-
Future<void> refreshData() async {
|
|
202
|
-
// 子类可以重写此方法实现刷新逻辑
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
`;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// provider 与 default 采用 ChangeNotifier 变体
|
|
209
|
-
return `import 'package:flutter/foundation.dart';
|
|
210
|
-
enum ViewState { idle, loading, success, error }
|
|
211
|
-
|
|
212
|
-
class BaseViewModel extends ChangeNotifier {
|
|
213
|
-
${commonInfo}
|
|
214
|
-
|
|
215
|
-
/// 通知数据变化(仅用于局部刷新)
|
|
216
|
-
void notifyDataChange() {
|
|
217
|
-
notifyListeners();
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// ==================== 生命周期 ====================
|
|
221
|
-
/// 初始化钩子(函数级注释)
|
|
222
|
-
Future<void> onInit() async {
|
|
223
|
-
// 子类可以重写此方法进行初始化
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/// 刷新数据(钩子)
|
|
227
|
-
Future<void> refreshData() async {
|
|
228
|
-
// 子类可以重写此方法实现刷新逻辑
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
`;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* 配置入口 main.dart:
|
|
236
|
-
* - 模板文件执行 Handlebars 渲染(只处理 riverpod 条件)
|
|
237
|
-
* - 非模板文件无需额外处理
|
|
238
|
-
*/
|
|
239
|
-
async configureMain () {
|
|
240
|
-
const mainPath = join(this.projectPath, 'lib', 'main.dart');
|
|
241
|
-
if (!(await fsx.pathExists(mainPath))) {
|
|
242
|
-
throw new Error(`main.dart not found at ${mainPath}`);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const raw = await fsx.readFile(mainPath, 'utf8');
|
|
246
|
-
|
|
247
|
-
// 检查是否为模板文件
|
|
248
|
-
const isTemplate = raw.includes('{{#if_riverpod}}');
|
|
249
|
-
if (isTemplate) {
|
|
250
|
-
this.registerHandlebarsHelpers();
|
|
251
|
-
const compiled = Handlebars.compile(raw);
|
|
252
|
-
const rendered = compiled({
|
|
253
|
-
if_riverpod: this.stateManager === 'riverpod'
|
|
254
|
-
});
|
|
255
|
-
await fsx.writeFile(mainPath, rendered);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* 增强 App 入口组件(GetX):
|
|
261
|
-
* - lite/clean:lib/app.dart → MaterialApp 替换为 GetMaterialApp,注入 get 导入与 getPages
|
|
262
|
-
* - modular:lib/main.dart → MaterialApp 替换为 GetMaterialApp,注入 get 导入与 getPages
|
|
263
|
-
*/
|
|
264
|
-
async configureAppWidget () {
|
|
265
|
-
try {
|
|
266
|
-
const tpl = detectProjectTemplate(this.projectPath);
|
|
267
|
-
const isGetX = this.stateManager === 'getx';
|
|
268
|
-
if (!isGetX) return;
|
|
269
|
-
|
|
270
|
-
if (tpl === 'lite' || tpl === 'clean') {
|
|
271
|
-
const appPath = join(this.projectPath, 'lib', 'app.dart');
|
|
272
|
-
if (!(await fsx.pathExists(appPath))) return;
|
|
273
|
-
let raw = await fsx.readFile(appPath, 'utf8');
|
|
274
|
-
if (!raw.includes('GetMaterialApp(')) {
|
|
275
|
-
raw = raw.replace(/MaterialApp\s*\(/, 'GetMaterialApp(');
|
|
276
|
-
}
|
|
277
|
-
if (!/package:get\/get\.dart/.test(raw)) {
|
|
278
|
-
raw = `import 'package:get/get.dart';\n` + raw;
|
|
279
|
-
}
|
|
280
|
-
const pagesAccessor = tpl === 'lite' ? 'AppRoutes.pages' : 'AppRoutes.pages';
|
|
281
|
-
if (!new RegExp(`getPages:\\s*${pagesAccessor}`).test(raw)) {
|
|
282
|
-
raw = raw.replace(/initialRoute:\s*AppRoutes\.home,/, `initialRoute: AppRoutes.home,\n getPages: ${pagesAccessor},`);
|
|
283
|
-
}
|
|
284
|
-
raw = raw.replace(/\s*routes:\s*AppRoutes\.routes,\s*\n/, '\n');
|
|
285
|
-
await fsx.writeFile(appPath, raw);
|
|
286
|
-
} else if (tpl === 'modular') {
|
|
287
|
-
const mainPath = join(this.projectPath, 'lib', 'main.dart');
|
|
288
|
-
if (!(await fsx.pathExists(mainPath))) return;
|
|
289
|
-
let raw = await fsx.readFile(mainPath, 'utf8');
|
|
290
|
-
if (!raw.includes('GetMaterialApp(')) {
|
|
291
|
-
raw = raw.replace(/MaterialApp\s*\(/, 'GetMaterialApp(');
|
|
292
|
-
}
|
|
293
|
-
if (!/package:get\/get\.dart/.test(raw)) {
|
|
294
|
-
raw = raw.replace(/^(import\s+['"].+?['"];\s*\n)+/m, (m) => `${m}import 'package:get/get.dart';\n`);
|
|
295
|
-
if (!raw.includes("import 'package:get/get.dart';")) {
|
|
296
|
-
raw = `import 'package:get/get.dart';\n` + raw;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
if (!/getPages:\s*AppRouter\.pages/.test(raw)) {
|
|
300
|
-
raw = raw.replace(/initialRoute:\s*AppRouter\.home,/, "initialRoute: AppRouter.home,\n getPages: AppRouter.pages,");
|
|
301
|
-
}
|
|
302
|
-
raw = raw.replace(/\s*routes:\s*AppRouter\.routes,\s*\n/, '\n');
|
|
303
|
-
await fsx.writeFile(mainPath, raw);
|
|
304
|
-
}
|
|
305
|
-
} catch (_) { }
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* 增强路由文件(GetX):为不同模板注入 List<GetPage> pages 与 get 导入
|
|
310
|
-
* 智能转换原有路由定义,保留所有路由
|
|
311
|
-
*/
|
|
312
|
-
async configureRoutes () {
|
|
313
|
-
try {
|
|
314
|
-
const tpl = detectProjectTemplate(this.projectPath);
|
|
315
|
-
if (this.stateManager !== 'getx') return;
|
|
316
|
-
let routesPath;
|
|
317
|
-
if (tpl === 'lite') {
|
|
318
|
-
routesPath = join(this.projectPath, 'lib', 'config', 'routes.dart');
|
|
319
|
-
} else if (tpl === 'modular') {
|
|
320
|
-
routesPath = join(this.projectPath, 'lib', 'core', 'router', 'app_router.dart');
|
|
321
|
-
} else if (tpl === 'clean') {
|
|
322
|
-
routesPath = join(this.projectPath, 'lib', 'config', 'routes', 'app_routes.dart');
|
|
323
|
-
} else {
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
if (!(await fsx.pathExists(routesPath))) return;
|
|
327
|
-
let raw = await fsx.readFile(routesPath, 'utf8');
|
|
328
|
-
|
|
329
|
-
// 替换 flutter import 为 get import
|
|
330
|
-
if (!/package:get\/get\.dart/.test(raw)) {
|
|
331
|
-
raw = raw.replace("import 'package:flutter/material.dart';\n", "import 'package:get/get.dart';\n");
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// 如果已经是 GetPage 格式,跳过
|
|
335
|
-
if (/static\s+List<\s*GetPage/.test(raw)) {
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// 提取路由常量定义
|
|
340
|
-
const routeConstantsMatch = raw.match(/static\s+const\s+String\s+\w+\s*=\s*['"][^'"]+['"];/g);
|
|
341
|
-
const routeConstants = routeConstantsMatch ? routeConstantsMatch.join('\n ') : "static const String home = '/';";
|
|
342
|
-
|
|
343
|
-
// 提取原有的 routes map 并转换为 GetPage
|
|
344
|
-
let getPages = [];
|
|
345
|
-
const routesMapMatch = raw.match(/routes:\s*\(context\)\s*=>\s*const\s+(\w+)\(\)/g) ||
|
|
346
|
-
raw.match(/(\w+):\s*\(context\)\s*=>\s*const\s+(\w+)\(\)/g);
|
|
347
|
-
|
|
348
|
-
if (routesMapMatch) {
|
|
349
|
-
// 从 routes getter 中提取
|
|
350
|
-
const routesGetterMatch = raw.match(/static\s+Map<String,\s*WidgetBuilder>\s+get\s+routes\s*\{[^}]*return\s*\{([^}]+)\};/s);
|
|
351
|
-
if (routesGetterMatch) {
|
|
352
|
-
const routesContent = routesGetterMatch[1];
|
|
353
|
-
// 解析每个路由
|
|
354
|
-
const routePattern = /(\w+):\s*\(context\)\s*=>\s*const\s+(\w+)\(\)/g;
|
|
355
|
-
let match;
|
|
356
|
-
while ((match = routePattern.exec(routesContent)) !== null) {
|
|
357
|
-
const [, routeName, widgetName] = match;
|
|
358
|
-
getPages.push(` GetPage(name: ${routeName}, page: () => const ${widgetName}())`);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// 如果没有提取到任何路由,使用默认的 home
|
|
364
|
-
if (getPages.length === 0) {
|
|
365
|
-
getPages.push(` GetPage(name: home, page: () => const HomePage())`);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
const className = raw.match(/class\s+(\w+)/)?.[1] || 'AppRoutes';
|
|
369
|
-
|
|
370
|
-
// 构建新的 class
|
|
371
|
-
const newClass = `class ${className} {
|
|
372
|
-
${className}._();
|
|
373
|
-
|
|
374
|
-
${routeConstants}
|
|
375
|
-
|
|
376
|
-
static List<GetPage<dynamic>> get pages {
|
|
377
|
-
return [
|
|
378
|
-
${getPages.join(',\n')},
|
|
379
|
-
];
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
`;
|
|
383
|
-
|
|
384
|
-
// 替换整个 class
|
|
385
|
-
raw = raw.replace(/class\s+\w+[\s\S]*?^}/m, newClass);
|
|
386
|
-
|
|
387
|
-
await fsx.writeFile(routesPath, raw);
|
|
388
|
-
} catch (error) {
|
|
389
|
-
logger.warn(` ⚠️ 路由配置可能需要手动调整: ${error.message}`);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
/**
|
|
394
|
-
* 注册 Handlebars 条件助手:控制模板片段的渲染/隐藏
|
|
395
|
-
*/
|
|
396
|
-
registerHandlebarsHelpers () {
|
|
397
|
-
Handlebars.registerHelper('if_riverpod', function (options) {
|
|
398
|
-
return options.data.root.if_riverpod ? options.fn(this) : options.inverse(this);
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
}
|
package/templates/README.md
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
# Flutter 模板
|
|
2
|
-
|
|
3
|
-
## 📁 独立仓库架构
|
|
4
|
-
|
|
5
|
-
从 V2 开始,每个模板都有自己的独立 Git 仓库:
|
|
6
|
-
|
|
7
|
-
| 模板 | 仓库地址 | 说明 |
|
|
8
|
-
| ----------- | -------------------------------------------------------------- | --------------------------------------- |
|
|
9
|
-
| **lite** | [template-lite](https://gitee.com/flu-cli/template-lite) | 轻量级模板,适合快速原型开发 |
|
|
10
|
-
| **modular** | [template-modular](https://gitee.com/flu-cli/template-modular) | 模块化模板,适合中大型项目 |
|
|
11
|
-
| **clean** | [template-clean](https://gitee.com/flu-cli/template-clean) | Clean Architecture 模板,适合企业级应用 |
|
|
12
|
-
|
|
13
|
-
## 🎯 为什么使用独立仓库?
|
|
14
|
-
|
|
15
|
-
### 优势
|
|
16
|
-
|
|
17
|
-
1. **独立维护**
|
|
18
|
-
|
|
19
|
-
- 每个模板可以独立更新和发版
|
|
20
|
-
- 不同团队可以维护不同模板
|
|
21
|
-
- 版本控制更清晰
|
|
22
|
-
|
|
23
|
-
2. **减小主仓库体积**
|
|
24
|
-
|
|
25
|
-
- flu-cli 主仓库不包含模板代码
|
|
26
|
-
- 下载和克隆更快
|
|
27
|
-
|
|
28
|
-
3. **灵活的版本控制**
|
|
29
|
-
|
|
30
|
-
- 用户可以选择特定版本的模板
|
|
31
|
-
- 支持模板的独立版本号
|
|
32
|
-
|
|
33
|
-
4. **更好的协作**
|
|
34
|
-
- 模板贡献者不需要访问主仓库
|
|
35
|
-
- 降低协作门槛
|
|
36
|
-
|
|
37
|
-
## 🚀 使用方式
|
|
38
|
-
|
|
39
|
-
### 通过 flu-cli 使用(推荐)
|
|
40
|
-
|
|
41
|
-
```bash
|
|
42
|
-
# flu-cli 会自动从远程下载模板
|
|
43
|
-
flu-cli new my_app --template lite
|
|
44
|
-
|
|
45
|
-
# 或使用本地模板(如果已克隆)
|
|
46
|
-
flu-cli new my_app --template lite
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
### 直接克隆模板
|
|
50
|
-
|
|
51
|
-
```bash
|
|
52
|
-
# 克隆模板仓库
|
|
53
|
-
git clone https://gitee.com/flu-cli/template-lite.git my_app
|
|
54
|
-
cd my_app
|
|
55
|
-
|
|
56
|
-
# 初始化(创建 pubspec.yaml)
|
|
57
|
-
./init.sh
|
|
58
|
-
|
|
59
|
-
# 运行
|
|
60
|
-
flutter run
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
## 📝 本地开发模板
|
|
64
|
-
|
|
65
|
-
如果你想在本地开发和测试模板:
|
|
66
|
-
|
|
67
|
-
```bash
|
|
68
|
-
# 1. 克隆模板到 templates/ 目录
|
|
69
|
-
cd templates/
|
|
70
|
-
git clone https://gitee.com/flu-cli/template-lite.git lite
|
|
71
|
-
git clone https://gitee.com/flu-cli/template-modular.git modular
|
|
72
|
-
git clone https://gitee.com/flu-cli/template-clean.git clean
|
|
73
|
-
|
|
74
|
-
# 2. 进入模板目录开发
|
|
75
|
-
cd lite
|
|
76
|
-
./init.sh
|
|
77
|
-
flutter run
|
|
78
|
-
|
|
79
|
-
# 3. 提交更改到模板仓库
|
|
80
|
-
git add .
|
|
81
|
-
git commit -m "Update template"
|
|
82
|
-
git push
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
## 🔄 模板更新流程
|
|
86
|
-
|
|
87
|
-
### 更新模板
|
|
88
|
-
|
|
89
|
-
```bash
|
|
90
|
-
# 进入模板目录
|
|
91
|
-
cd templates/lite
|
|
92
|
-
|
|
93
|
-
# 拉取最新代码
|
|
94
|
-
git pull
|
|
95
|
-
|
|
96
|
-
# 测试
|
|
97
|
-
./init.sh
|
|
98
|
-
flutter run
|
|
99
|
-
|
|
100
|
-
# 提交更改
|
|
101
|
-
git add .
|
|
102
|
-
git commit -m "Update: xxx"
|
|
103
|
-
git push
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
### 用户更新模板
|
|
107
|
-
|
|
108
|
-
```bash
|
|
109
|
-
# flu-cli 会自动检查更新
|
|
110
|
-
flu-cli update
|
|
111
|
-
|
|
112
|
-
# 或强制更新
|
|
113
|
-
flu-cli new my_app --template lite --no-cache
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
## 📚 相关文档
|
|
117
|
-
|
|
118
|
-
- [双文件策略说明](../MIGRATION_PLAN.md#双文件策略)
|
|
119
|
-
- [迁移到 Gitee 组织](../MIGRATION_PLAN.md)
|
|
120
|
-
- [模板开发指南](https://gitee.com/flu-cli/flu-cli-docs)
|
|
121
|
-
|
|
122
|
-
## 💡 注意事项
|
|
123
|
-
|
|
124
|
-
1. **flu-cli 主仓库不跟踪 templates/ 内容**
|
|
125
|
-
|
|
126
|
-
- templates/ 已添加到 .gitignore
|
|
127
|
-
- 只保留此 README.md
|
|
128
|
-
|
|
129
|
-
2. **每个模板都是独立的 Git 仓库**
|
|
130
|
-
|
|
131
|
-
- 有自己的 .git 目录
|
|
132
|
-
- 独立的提交历史
|
|
133
|
-
- 独立的版本号
|
|
134
|
-
|
|
135
|
-
3. **本地开发时**
|
|
136
|
-
- 可以在 templates/ 下克隆模板仓库
|
|
137
|
-
- 修改后提交到对应的模板仓库
|
|
138
|
-
- 不会影响 flu-cli 主仓库
|