flu-cli 0.0.5 → 2.0.1
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/CLI.md +512 -0
- package/README.md +70 -260
- package/config/dev.config.js +56 -0
- package/config/templates.js +147 -0
- package/index.js +177 -81
- package/lib/commands/add.js +595 -0
- package/lib/commands/cache.js +99 -0
- package/lib/commands/completion.js +92 -0
- package/lib/commands/config.js +70 -0
- package/lib/commands/newClack.js +399 -0
- package/lib/commands/snippets.js +39 -0
- package/lib/commands/template.js +113 -0
- package/lib/commands/templates.js +84 -0
- package/lib/generators/model_generator.js +303 -0
- package/lib/generators/page_generator.js +322 -0
- package/lib/generators/project_generator.js +96 -0
- package/lib/generators/service_generator.js +408 -0
- package/lib/generators/state_manager_generator.js +402 -0
- package/lib/generators/viewmodel_generator.js +115 -0
- package/lib/generators/widget_generator.js +104 -0
- package/lib/templates/templateCopier.js +296 -0
- package/lib/templates/templateManager.js +191 -0
- package/lib/utils/config.js +99 -0
- package/lib/utils/flutterHelper.js +85 -0
- package/lib/utils/index_updater.js +69 -0
- package/lib/utils/logger.js +57 -0
- package/lib/utils/project_detector.js +227 -0
- package/lib/utils/snippet_loader.js +32 -0
- package/lib/utils/string_helper.js +56 -0
- package/lib/utils/templateSelectorEnquirer.js +203 -0
- package/package.json +34 -7
- package/release.sh +107 -0
- package/scripts/e2e-state-tests.js +116 -0
- package/scripts/sync-base-to-templates.js +108 -0
- package/scripts/workspace-clone-all.sh +101 -0
- package/scripts/workspace-status-all.sh +112 -0
- package/templates/README.md +138 -0
- package/templates/base_files/base_list_page.dart.template +174 -0
- package/templates/base_files/base_list_viewmodel.dart.template +134 -0
- package/templates/base_files/base_page.dart.template +251 -0
- package/templates/base_files/base_viewmodel.dart.template +77 -0
- package/templates/base_files/theme/status_views_theme.dart.template +46 -0
- package/templates/snippets/dart.code-snippets +392 -0
- package/lib/createProject.js +0 -220
- package/lib/flutterProjectCreator.js +0 -80
- package/lib/libCopier.js +0 -368
- package/lib/userInteraction.js +0 -274
- package/lib/utils.js +0 -200
- package/publish.sh +0 -29
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 页面生成器
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
6
|
+
import { join, relative, dirname } from 'path';
|
|
7
|
+
import { logger } from '../utils/logger.js';
|
|
8
|
+
import { toPascalCase, toSnakeCase, toTitleCase } from '../utils/string_helper.js';
|
|
9
|
+
import { updateIndexFile } from '../utils/index_updater.js';
|
|
10
|
+
import { detectProjectTemplate, getPagePath, getViewModelPath } from '../utils/project_detector.js';
|
|
11
|
+
import { getSnippetContent } from '../utils/snippet_loader.js';
|
|
12
|
+
import { generateModel } from './model_generator.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 生成页面文件
|
|
16
|
+
*/
|
|
17
|
+
export function generatePage (name, options = {}) {
|
|
18
|
+
try {
|
|
19
|
+
const {
|
|
20
|
+
feature = null,
|
|
21
|
+
stateful = false,
|
|
22
|
+
stateless = false,
|
|
23
|
+
withViewModel = true,
|
|
24
|
+
isListPage = false,
|
|
25
|
+
outputDir = process.cwd()
|
|
26
|
+
} = options;
|
|
27
|
+
|
|
28
|
+
// 检测项目模板类型
|
|
29
|
+
const template = detectProjectTemplate(outputDir);
|
|
30
|
+
logger.info(`检测到项目类型: ${template || '未知'}`);
|
|
31
|
+
|
|
32
|
+
// 转换命名
|
|
33
|
+
const namePascal = toPascalCase(name);
|
|
34
|
+
const nameSnake = toSnakeCase(name);
|
|
35
|
+
const nameTitle = toTitleCase(name);
|
|
36
|
+
|
|
37
|
+
// 确定模块名称
|
|
38
|
+
let moduleName = feature || name;
|
|
39
|
+
|
|
40
|
+
// 根据模板类型确定输出路径
|
|
41
|
+
const pagesDir = getPagePath(outputDir, moduleName);
|
|
42
|
+
|
|
43
|
+
// 创建目录
|
|
44
|
+
if (!existsSync(pagesDir)) {
|
|
45
|
+
mkdirSync(pagesDir, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 获取 ViewModel 路径用于计算相对导入
|
|
49
|
+
const vmDir = getViewModelPath(outputDir, moduleName);
|
|
50
|
+
const pageFilePath = join(pagesDir, `${nameSnake}_page.dart`);
|
|
51
|
+
const vmFilePath = join(vmDir, `${nameSnake}_viewmodel.dart`);
|
|
52
|
+
|
|
53
|
+
// 计算相对导入路径
|
|
54
|
+
const vmImportPath = calculateRelativeImport(pageFilePath, vmFilePath);
|
|
55
|
+
|
|
56
|
+
// 生成文件内容(优先使用项目片段)
|
|
57
|
+
const variables = {
|
|
58
|
+
Name: namePascal,
|
|
59
|
+
name: nameSnake.replace(/_([a-z])/g, (_, c) => c.toUpperCase()),
|
|
60
|
+
snake_name: nameSnake,
|
|
61
|
+
title: nameTitle,
|
|
62
|
+
vm_import: withViewModel ? vmImportPath : '',
|
|
63
|
+
ModelName: namePascal + 'Model' // 默认 Model 名称
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// 智能选择片段:
|
|
67
|
+
let content = null;
|
|
68
|
+
let key;
|
|
69
|
+
|
|
70
|
+
if (isListPage) {
|
|
71
|
+
// 列表页
|
|
72
|
+
key = 'flu.listPage';
|
|
73
|
+
logger.info('生成类型: ListPage (BaseListPage) + ViewModel');
|
|
74
|
+
} else if (stateless) {
|
|
75
|
+
// 强制使用 StatelessWidget
|
|
76
|
+
key = 'flu.lessPage';
|
|
77
|
+
if (withViewModel) {
|
|
78
|
+
logger.warn('⚠️ 警告: 使用 StatelessWidget + ViewModel 组合,无法监听状态变化');
|
|
79
|
+
}
|
|
80
|
+
logger.info('生成类型: StatelessWidget (强制)');
|
|
81
|
+
} else if (stateful) {
|
|
82
|
+
// 明确指定 StatefulWidget
|
|
83
|
+
key = 'flu.stPage';
|
|
84
|
+
logger.info(`生成类型: StatefulWidget${withViewModel ? ' (BasePage) + ViewModel' : ''}`);
|
|
85
|
+
} else if (withViewModel) {
|
|
86
|
+
// 有 ViewModel 时,默认使用 StatefulWidget (BasePage)
|
|
87
|
+
key = 'flu.stPage';
|
|
88
|
+
logger.info('生成类型: StatefulWidget (BasePage) + ViewModel');
|
|
89
|
+
} else {
|
|
90
|
+
// 无 ViewModel 且未指定,默认 Stateless
|
|
91
|
+
key = 'flu.lessPage';
|
|
92
|
+
logger.info('生成类型: StatelessWidget (无 ViewModel)');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
content = getSnippetContent(outputDir, key, variables);
|
|
96
|
+
|
|
97
|
+
if (!content) {
|
|
98
|
+
// 如果没有片段,使用默认生成逻辑 (这里简化处理,列表页必须有片段)
|
|
99
|
+
if (isListPage) {
|
|
100
|
+
logger.error('❌ 错误: 找不到列表页代码片段 (flu.listPage)');
|
|
101
|
+
logger.info('提示: 请运行 flu-cli sync-snippets 更新代码片段');
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
content = generatePageContent(namePascal, nameSnake, nameTitle, {
|
|
105
|
+
stateful,
|
|
106
|
+
withViewModel,
|
|
107
|
+
vmImportPath,
|
|
108
|
+
template
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 写入文件
|
|
113
|
+
if (existsSync(pageFilePath)) {
|
|
114
|
+
logger.error(`文件已存在: ${pageFilePath}`);
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
writeFileSync(pageFilePath, content, 'utf8');
|
|
119
|
+
logger.success(`页面创建成功: ${pageFilePath}`);
|
|
120
|
+
|
|
121
|
+
// 更新 index.dart
|
|
122
|
+
updateIndexFile(pagesDir, `${nameSnake}_page.dart`);
|
|
123
|
+
|
|
124
|
+
// 如果是列表页,先生成 Model
|
|
125
|
+
if (isListPage) {
|
|
126
|
+
logger.info('列表页需要 Model,正在自动生成...');
|
|
127
|
+
// 只有 modular/clean 架构才使用 feature
|
|
128
|
+
const modelFeature = (template === 'modular' || template === 'clean') ? moduleName : null;
|
|
129
|
+
generateModel(name, { feature: modelFeature, outputDir });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 如果需要 ViewModel,也生成它
|
|
133
|
+
if (withViewModel) {
|
|
134
|
+
generateViewModel(name, { feature: moduleName, outputDir, template, isListPage });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return true;
|
|
138
|
+
|
|
139
|
+
} catch (error) {
|
|
140
|
+
logger.error(`生成页面失败: ${error.message}`);
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 计算相对导入路径
|
|
147
|
+
*/
|
|
148
|
+
function calculateRelativeImport (fromFile, toFile) {
|
|
149
|
+
const fromDir = dirname(fromFile);
|
|
150
|
+
const relPath = relative(fromDir, toFile);
|
|
151
|
+
return relPath.replace(/\\/g, '/');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 生成页面内容
|
|
156
|
+
*/
|
|
157
|
+
function generatePageContent (namePascal, nameSnake, nameTitle, options) {
|
|
158
|
+
const { stateful, withViewModel, vmImportPath, template } = options;
|
|
159
|
+
|
|
160
|
+
let imports = `import 'package:flutter/material.dart';\n`;
|
|
161
|
+
|
|
162
|
+
// StatefulWidget with BasePage
|
|
163
|
+
if (stateful && withViewModel) {
|
|
164
|
+
// 导入 BasePage
|
|
165
|
+
const baseImport = template === 'lite'
|
|
166
|
+
? '../base/base_page.dart'
|
|
167
|
+
: '../../core/base/base_page.dart';
|
|
168
|
+
|
|
169
|
+
imports += `import '${baseImport}';\n`;
|
|
170
|
+
imports += `import '${vmImportPath}';\n`;
|
|
171
|
+
|
|
172
|
+
return `${imports}
|
|
173
|
+
class ${namePascal}Page extends BasePage<${namePascal}ViewModel> {
|
|
174
|
+
const ${namePascal}Page({super.key});
|
|
175
|
+
|
|
176
|
+
@override
|
|
177
|
+
State<${namePascal}Page> createState() => _${namePascal}PageState();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
class _${namePascal}PageState extends BasePageState<${namePascal}ViewModel, ${namePascal}Page> {
|
|
181
|
+
// ==================== UI 配置 ====================
|
|
182
|
+
@override
|
|
183
|
+
String get title => '${nameTitle}';
|
|
184
|
+
|
|
185
|
+
// ==================== ViewModel ====================
|
|
186
|
+
@override
|
|
187
|
+
${namePascal}ViewModel createViewModel() => ${namePascal}ViewModel();
|
|
188
|
+
|
|
189
|
+
// ==================== UI 构建 ====================
|
|
190
|
+
@override
|
|
191
|
+
Widget buildContent(BuildContext context) {
|
|
192
|
+
return Center(
|
|
193
|
+
child: Text('${namePascal}Page'),
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
`;
|
|
198
|
+
} else if (stateful) {
|
|
199
|
+
// 普通 StatefulWidget (无 ViewModel)
|
|
200
|
+
return `${imports}
|
|
201
|
+
class ${namePascal}Page extends StatefulWidget {
|
|
202
|
+
const ${namePascal}Page({super.key});
|
|
203
|
+
|
|
204
|
+
@override
|
|
205
|
+
State<${namePascal}Page> createState() => _${namePascal}PageState();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
class _${namePascal}PageState extends State<${namePascal}Page> {
|
|
209
|
+
@override
|
|
210
|
+
Widget build(BuildContext context) {
|
|
211
|
+
return Scaffold(
|
|
212
|
+
appBar: AppBar(
|
|
213
|
+
title: const Text('${nameTitle}'),
|
|
214
|
+
),
|
|
215
|
+
body: Center(
|
|
216
|
+
child: Text('${namePascal}Page'),
|
|
217
|
+
),
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
`;
|
|
222
|
+
} else {
|
|
223
|
+
// StatelessWidget
|
|
224
|
+
return `${imports}
|
|
225
|
+
class ${namePascal}Page extends StatelessWidget {
|
|
226
|
+
const ${namePascal}Page({super.key});
|
|
227
|
+
|
|
228
|
+
@override
|
|
229
|
+
Widget build(BuildContext context) {
|
|
230
|
+
return Scaffold(
|
|
231
|
+
appBar: AppBar(
|
|
232
|
+
title: const Text('${nameTitle}'),
|
|
233
|
+
),
|
|
234
|
+
body: Center(
|
|
235
|
+
child: Text('${namePascal}Page'),
|
|
236
|
+
),
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
`;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 生成 ViewModel
|
|
246
|
+
*/
|
|
247
|
+
function generateViewModel (name, options = {}) {
|
|
248
|
+
const { feature = null, outputDir = process.cwd(), template = 'lite', isListPage = false } = options;
|
|
249
|
+
|
|
250
|
+
const namePascal = toPascalCase(name);
|
|
251
|
+
const nameSnake = toSnakeCase(name);
|
|
252
|
+
|
|
253
|
+
// 根据模板类型确定输出路径
|
|
254
|
+
const vmDir = getViewModelPath(outputDir, feature);
|
|
255
|
+
|
|
256
|
+
if (!existsSync(vmDir)) {
|
|
257
|
+
mkdirSync(vmDir, { recursive: true });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 根据片段或模板类型生成内容
|
|
261
|
+
const snippetKey = isListPage ? 'flu.listViewModel' : 'flu.viewmodel';
|
|
262
|
+
const vmSnippet = getSnippetContent(outputDir, snippetKey, {
|
|
263
|
+
Name: namePascal,
|
|
264
|
+
snake_name: nameSnake,
|
|
265
|
+
ModelName: namePascal + 'Model'
|
|
266
|
+
});
|
|
267
|
+
let content = vmSnippet;
|
|
268
|
+
if (template === 'clean') {
|
|
269
|
+
// Clean 架构的 ViewModel 需要依赖 UseCase
|
|
270
|
+
content = content || `import 'package:flutter/foundation.dart';
|
|
271
|
+
|
|
272
|
+
class ${namePascal}ViewModel extends ChangeNotifier {
|
|
273
|
+
// TODO: 注入 UseCase
|
|
274
|
+
// final GetSomething getSomething;
|
|
275
|
+
|
|
276
|
+
// ${namePascal}ViewModel({
|
|
277
|
+
// required this.getSomething,
|
|
278
|
+
// });
|
|
279
|
+
|
|
280
|
+
void init() {
|
|
281
|
+
// 初始化逻辑
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
@override
|
|
285
|
+
void dispose() {
|
|
286
|
+
// 清理资源
|
|
287
|
+
super.dispose();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
`;
|
|
291
|
+
} else {
|
|
292
|
+
// Lite 和 Modular 使用标准的 ViewModel
|
|
293
|
+
content = content || `import 'package:flutter/foundation.dart';
|
|
294
|
+
|
|
295
|
+
class ${namePascal}ViewModel extends ChangeNotifier {
|
|
296
|
+
void init() {
|
|
297
|
+
// 初始化逻辑
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
@override
|
|
301
|
+
void dispose() {
|
|
302
|
+
// 清理资源
|
|
303
|
+
super.dispose();
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
`;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const filePath = join(vmDir, `${nameSnake}_viewmodel.dart`);
|
|
310
|
+
if (existsSync(filePath)) {
|
|
311
|
+
logger.warn(`ViewModel 已存在: ${filePath}`);
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
writeFileSync(filePath, content, 'utf8');
|
|
316
|
+
logger.success(`ViewModel 创建成功: ${filePath}`);
|
|
317
|
+
|
|
318
|
+
// 更新 index.dart
|
|
319
|
+
updateIndexFile(vmDir, `${nameSnake}_viewmodel.dart`);
|
|
320
|
+
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
}
|