flu-cli 0.0.4 → 2.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.
Files changed (50) hide show
  1. package/CLI.md +349 -0
  2. package/README.md +59 -155
  3. package/config/dev.config.js +56 -0
  4. package/config/templates.js +147 -0
  5. package/index.js +128 -81
  6. package/lib/commands/add.js +472 -0
  7. package/lib/commands/cache.js +99 -0
  8. package/lib/commands/completion.js +94 -0
  9. package/lib/commands/generate.js +26 -0
  10. package/lib/commands/newClack.js +396 -0
  11. package/lib/commands/snippets.js +39 -0
  12. package/lib/commands/templates.js +84 -0
  13. package/lib/generators/component_generator.js +93 -0
  14. package/lib/generators/model_generator.js +303 -0
  15. package/lib/generators/module_generator.js +141 -0
  16. package/lib/generators/page_generator.js +322 -0
  17. package/lib/generators/project_generator.js +96 -0
  18. package/lib/generators/service_generator.js +408 -0
  19. package/lib/generators/state_manager_generator.js +402 -0
  20. package/lib/generators/viewmodel_generator.js +115 -0
  21. package/lib/generators/widget_generator.js +104 -0
  22. package/lib/templates/templateCopier.js +296 -0
  23. package/lib/templates/templateManager.js +191 -0
  24. package/lib/utils/config.js +99 -0
  25. package/lib/utils/flutterHelper.js +85 -0
  26. package/lib/utils/index_updater.js +69 -0
  27. package/lib/utils/logger.js +57 -0
  28. package/lib/utils/project_detector.js +227 -0
  29. package/lib/utils/snippet_loader.js +32 -0
  30. package/lib/utils/string_helper.js +56 -0
  31. package/lib/utils/templateSelectorEnquirer.js +200 -0
  32. package/package.json +31 -6
  33. package/release.sh +107 -0
  34. package/scripts/e2e-state-tests.js +116 -0
  35. package/scripts/sync-base-to-templates.js +108 -0
  36. package/scripts/workspace-clone-all.sh +101 -0
  37. package/scripts/workspace-status-all.sh +112 -0
  38. package/templates/README.md +138 -0
  39. package/templates/base_files/base_list_page.dart.template +174 -0
  40. package/templates/base_files/base_list_viewmodel.dart.template +134 -0
  41. package/templates/base_files/base_page.dart.template +251 -0
  42. package/templates/base_files/base_viewmodel.dart.template +77 -0
  43. package/templates/base_files/theme/status_views_theme.dart.template +46 -0
  44. package/templates/snippets/dart.code-snippets +487 -0
  45. package/lib/createProject.js +0 -220
  46. package/lib/flutterProjectCreator.js +0 -80
  47. package/lib/libCopier.js +0 -368
  48. package/lib/userInteraction.js +0 -274
  49. package/lib/utils.js +0 -200
  50. package/publish.sh +0 -29
@@ -0,0 +1,396 @@
1
+ /**
2
+ * new 命令(简化版)
3
+ * 职责:交互创建 Flutter 项目、选择模板与状态管理器,并触发生成器落地
4
+ * 说明:保留平台与包名,模板文件覆盖通用代码;支持内置/自定义模板与缓存开关
5
+ */
6
+
7
+ import * as p from '@clack/prompts';
8
+ import chalk from 'chalk';
9
+ import { join, dirname } from 'path';
10
+ import { fileURLToPath } from 'url';
11
+ import { existsSync } from 'fs';
12
+ import { getTemplate, isValidTemplate } from '../../config/templates.js';
13
+ import { selectTemplateWithEnquirer } from '../utils/templateSelectorEnquirer.js';
14
+ import { cloneOrUpdateTemplate } from '../templates/templateManager.js';
15
+ import { getAuthorName, saveAuthorName, findCustomTemplateById, saveDefaultTemplate } from '../utils/config.js';
16
+ import { copyTemplate, replaceVariables, copyCustomTemplate, ensurePubspecName, mergePubspecFromTemplate, cleanupTemplateFiles } from '../templates/templateCopier.js';
17
+ import { runFlutterCreate } from '../utils/flutterHelper.js';
18
+ import { ProjectGenerator } from '../generators/project_generator.js';
19
+ import { syncSnippets } from './snippets.js';
20
+
21
+ /**
22
+ * 创建新项目
23
+ */
24
+ export async function newProjectWithClack (projectName, options) {
25
+ p.intro(chalk.cyan.bold('🚀 创建 Flutter 项目'));
26
+
27
+ try {
28
+ const isInteractive = !projectName;
29
+ let finalProjectName = projectName;
30
+ let templateSelection = null;
31
+ let stateManager = options.state || 'default';
32
+ const nonInteractive = process.env.FLU_CLI_NON_INTERACTIVE === '1';
33
+
34
+ // ========== 交互式模式 ==========
35
+ if (isInteractive) {
36
+ // Step 1: 项目名称
37
+ finalProjectName = await p.text({
38
+ message: '请输入项目名称(直接回车使用: my_app)',
39
+ placeholder: 'my_app'
40
+ });
41
+
42
+ if (p.isCancel(finalProjectName)) {
43
+ p.cancel('操作已取消');
44
+ process.exit(0);
45
+ }
46
+
47
+ if (!finalProjectName || finalProjectName === '') {
48
+ finalProjectName = 'my_app';
49
+ }
50
+
51
+ // Step 2: 选择模板(实时预览,支持内置/自定义)
52
+ templateSelection = await selectTemplateWithEnquirer();
53
+
54
+ // Step 2.5: 选择状态管理器
55
+ const smChoice = await p.select({
56
+ message: '请选择状态管理器',
57
+ options: [
58
+ { value: 'default', label: 'ChangeNotifier (默认)' },
59
+ { value: 'provider', label: 'Provider' },
60
+ { value: 'getx', label: 'GetX' },
61
+ { value: 'riverpod', label: 'Riverpod' }
62
+ ],
63
+ initialValue: 'default'
64
+ });
65
+ if (!p.isCancel(smChoice)) {
66
+ stateManager = smChoice;
67
+ }
68
+
69
+ // Step 3: 项目路径(含冲突检测与删除确认)
70
+ const defaultPath = join(options.dir, finalProjectName);
71
+ let projectPath = await p.text({
72
+ message: `请输入项目路径(直接回车使用: ${defaultPath})`,
73
+ placeholder: defaultPath
74
+ });
75
+
76
+ if (p.isCancel(projectPath)) {
77
+ p.cancel('操作已取消');
78
+ process.exit(0);
79
+ }
80
+
81
+ if (!projectPath || projectPath === '') {
82
+ projectPath = defaultPath;
83
+ }
84
+
85
+ // Step 4: 包名
86
+ const defaultPackageName = `com.example.${finalProjectName}`;
87
+ let packageName = await p.text({
88
+ message: `请输入包名(直接回车使用: ${defaultPackageName})`,
89
+ placeholder: defaultPackageName
90
+ });
91
+
92
+ if (p.isCancel(packageName)) {
93
+ p.cancel('操作已取消');
94
+ process.exit(0);
95
+ }
96
+
97
+ if (!packageName || packageName === '') {
98
+ packageName = defaultPackageName;
99
+ }
100
+
101
+ // Step 5: 作者(使用缓存的名字)
102
+ const cachedAuthor = getAuthorName();
103
+ let author = await p.text({
104
+ message: `请输入作者名称(直接回车使用: ${cachedAuthor})`,
105
+ placeholder: cachedAuthor
106
+ });
107
+
108
+ if (p.isCancel(author)) {
109
+ p.cancel('操作已取消');
110
+ process.exit(0);
111
+ }
112
+
113
+ if (!author || author === '') {
114
+ author = cachedAuthor;
115
+ } else {
116
+ // 保存新的作者名字
117
+ saveAuthorName(author);
118
+ }
119
+
120
+ // Step 6: 检查目录是否存在(交互确认删除)
121
+ if (existsSync(projectPath)) {
122
+ console.log('');
123
+ console.log(chalk.yellow(`⚠️ 目录已存在: ${projectPath}`));
124
+
125
+ const shouldDelete = await p.confirm({
126
+ message: '是否删除现有目录并重新创建?',
127
+ initialValue: true
128
+ });
129
+
130
+ if (p.isCancel(shouldDelete)) {
131
+ p.cancel('操作已取消');
132
+ process.exit(0);
133
+ }
134
+
135
+ if (shouldDelete) {
136
+ const { rmSync } = await import('fs');
137
+ try {
138
+ rmSync(projectPath, { recursive: true, force: true });
139
+ console.log(chalk.green(`✓ 已删除目录: ${projectPath}`));
140
+ } catch (error) {
141
+ p.cancel(`删除目录失败: ${error.message}`);
142
+ process.exit(1);
143
+ }
144
+ } else {
145
+ p.cancel('操作已取消');
146
+ process.exit(0);
147
+ }
148
+ }
149
+
150
+ // Step 7: 显示信息并确认
151
+ const templateDisplay = templateSelection?.kind === 'builtin'
152
+ ? getTemplate(templateSelection.name).displayName
153
+ : (findCustomTemplateById(templateSelection.id)?.name || '自定义模板');
154
+
155
+ console.log('');
156
+ console.log(chalk.cyan('📋 项目信息确认:'));
157
+ console.log('');
158
+ console.log(` ${chalk.gray('项目名称:')} ${chalk.green(finalProjectName)}`);
159
+ console.log(` ${chalk.gray('项目模板:')} ${chalk.green(templateDisplay)}`);
160
+ console.log(` ${chalk.gray('项目路径:')} ${chalk.green(projectPath)}`);
161
+ console.log(` ${chalk.gray('包名:')} ${chalk.green(packageName)}`);
162
+ console.log(` ${chalk.gray('作者:')} ${chalk.green(author)}`);
163
+ console.log('');
164
+
165
+ const shouldContinue = await p.confirm({
166
+ message: '确认创建项目?'
167
+ });
168
+
169
+ if (!shouldContinue || p.isCancel(shouldContinue)) {
170
+ p.cancel('操作已取消');
171
+ process.exit(0);
172
+ }
173
+
174
+ // Step 8: 创建项目
175
+ await createProject(finalProjectName, templateSelection, projectPath, { projectName: finalProjectName, packageName, author, stateManager }, options);
176
+
177
+ } else {
178
+ // ========== 命令行模式 ==========
179
+ if (!isValidProjectName(finalProjectName)) {
180
+ p.cancel('项目名称只能包含小写字母、数字和下划线');
181
+ process.exit(1);
182
+ }
183
+
184
+ // 如果通过参数传入模板名称,构造为内置模板选择
185
+ if (!templateSelection && options.template) {
186
+ templateSelection = { kind: 'builtin', name: options.template };
187
+ }
188
+ if (!templateSelection) {
189
+ templateSelection = await selectTemplateWithEnquirer();
190
+ }
191
+ if (templateSelection.kind === 'builtin' && !isValidTemplate(templateSelection.name)) {
192
+ p.cancel(`模板 "${templateSelection.name}" 不存在`);
193
+ process.exit(1);
194
+ }
195
+
196
+ const projectDir = join(options.dir, finalProjectName);
197
+
198
+ // 检查目录是否存在(命令行模式也提供删除确认)
199
+ if (existsSync(projectDir)) {
200
+ if (nonInteractive) {
201
+ const { rmSync } = await import('fs');
202
+ rmSync(projectDir, { recursive: true, force: true });
203
+ } else {
204
+ console.log('');
205
+ console.log(chalk.yellow(`⚠️ 目录已存在: ${projectDir}`));
206
+ const shouldDelete = await p.confirm({ message: '是否删除现有目录并重新创建?', initialValue: true });
207
+ if (p.isCancel(shouldDelete)) {
208
+ p.cancel('操作已取消');
209
+ process.exit(0);
210
+ }
211
+ if (shouldDelete) {
212
+ const { rmSync } = await import('fs');
213
+ try {
214
+ rmSync(projectDir, { recursive: true, force: true });
215
+ console.log(chalk.green(`✓ 已删除目录: ${projectDir}`));
216
+ } catch (error) {
217
+ p.cancel(`删除目录失败: ${error.message}`);
218
+ process.exit(1);
219
+ }
220
+ } else {
221
+ p.cancel('操作已取消');
222
+ process.exit(0);
223
+ }
224
+ }
225
+ }
226
+
227
+ const defaultPackageName = `com.example.${finalProjectName}`;
228
+ let packageName = defaultPackageName;
229
+ if (!nonInteractive) {
230
+ packageName = await p.text({ message: `请输入包名(直接回车使用: ${defaultPackageName})`, placeholder: defaultPackageName });
231
+ if (p.isCancel(packageName)) {
232
+ p.cancel('操作已取消');
233
+ process.exit(0);
234
+ }
235
+ if (!packageName || packageName === '') {
236
+ packageName = defaultPackageName;
237
+ }
238
+ }
239
+
240
+ let author = 'Your Name';
241
+ if (!nonInteractive) {
242
+ author = await p.text({ message: '请输入作者名称(直接回车使用: Your Name)', placeholder: 'Your Name' });
243
+ if (p.isCancel(author)) {
244
+ p.cancel('操作已取消');
245
+ process.exit(0);
246
+ }
247
+ if (!author || author === '') {
248
+ author = 'Your Name';
249
+ }
250
+ }
251
+
252
+ // CLI 模式支持 --state 传参
253
+ await createProject(finalProjectName, templateSelection, projectDir, { projectName: finalProjectName, packageName, author, stateManager }, options);
254
+ }
255
+ } catch (error) {
256
+ p.cancel(`创建失败: ${error.message}`);
257
+ process.exit(1);
258
+ }
259
+ }
260
+
261
+ /**
262
+ * 创建项目
263
+ */
264
+ async function createProject (projectName, templateSelection, projectDir, projectInfo, options) {
265
+ const s = p.spinner();
266
+
267
+ try {
268
+ // 1. 准备模板
269
+ s.start('正在准备模板...');
270
+ let templatePath;
271
+ if (templateSelection?.kind === 'builtin') {
272
+ const template = getTemplate(templateSelection.name);
273
+ // 根据运行环境决定是否使用本地模板
274
+ // 生产环境(production)默认走远程,其他环境默认走本地(除非显式使用 --remote)
275
+ const runMode = process.env.FLU_CLI_MODE || process.env.NODE_ENV || 'development';
276
+ const useLocal = !options.remote && runMode !== 'production' && (template.local || false);
277
+ const __here = dirname(fileURLToPath(import.meta.url));
278
+ const cliRoot = dirname(dirname(__here)); // flu-cli/flu-cli
279
+ const workspaceRoot = dirname(cliRoot);
280
+ const sourceLocalPath = join(workspaceRoot, `template-${templateSelection.name}`);
281
+ const forceUpdate = useLocal ? true : !options.cache;
282
+ templatePath = await cloneOrUpdateTemplate(
283
+ templateSelection.name,
284
+ template.repo,
285
+ template.branch,
286
+ forceUpdate,
287
+ useLocal,
288
+ sourceLocalPath
289
+ );
290
+ } else if (templateSelection?.kind === 'custom') {
291
+ const ct = findCustomTemplateById(templateSelection.id);
292
+ if (!ct) {
293
+ s.stop('模板准备失败');
294
+ throw new Error('未找到自定义模板');
295
+ }
296
+ if (ct.type === 'local') {
297
+ templatePath = await cloneOrUpdateTemplate(
298
+ ct.id,
299
+ null,
300
+ 'main',
301
+ !options.cache,
302
+ true,
303
+ ct.pathOrUrl
304
+ );
305
+ } else {
306
+ templatePath = await cloneOrUpdateTemplate(
307
+ ct.id,
308
+ ct.pathOrUrl,
309
+ ct.branch || 'main',
310
+ !options.cache,
311
+ false
312
+ );
313
+ }
314
+ } else {
315
+ throw new Error('模板选择无效');
316
+ }
317
+
318
+ if (!templatePath) {
319
+ s.stop('模板准备失败');
320
+ throw new Error('模板准备失败');
321
+ }
322
+ s.stop('✓ 模板准备完成');
323
+
324
+ // 2. 创建 Flutter 项目
325
+ s.start('正在创建 Flutter 项目...');
326
+ const created = await runFlutterCreate(projectDir, projectInfo.projectName, projectInfo.packageName);
327
+ if (created) {
328
+ s.stop('✓ Flutter 项目创建成功');
329
+ } else {
330
+ s.stop('Flutter 项目创建失败');
331
+ }
332
+
333
+ // 3. 复制模板文件(自定义模板仅覆盖通用代码,保留平台与包名)
334
+ s.start('正在复制模板文件...');
335
+ const isCustom = templateSelection?.kind === 'custom';
336
+ const copied = isCustom
337
+ ? await copyCustomTemplate(templatePath, projectDir)
338
+ : await copyTemplate(templatePath, projectDir);
339
+
340
+ if (!copied) {
341
+ s.stop('模板文件复制失败');
342
+ throw new Error('模板文件复制失败');
343
+ }
344
+ s.stop('✓ 模板文件复制成功');
345
+
346
+ // 4. 配置项目
347
+ s.start('正在配置项目...');
348
+ const pg = new ProjectGenerator();
349
+ await pg.processTemplates(projectDir);
350
+ await replaceVariables(projectDir, projectInfo);
351
+ if (isCustom) {
352
+ mergePubspecFromTemplate(templatePath, projectDir, projectInfo.projectName);
353
+ } else {
354
+ ensurePubspecName(projectDir, projectInfo.projectName);
355
+ }
356
+ // 配置状态管理器
357
+ await pg.configureStateManager(projectDir, projectInfo.stateManager || 'default');
358
+
359
+ // 同步标准代码片段
360
+ s.start('正在同步代码片段...');
361
+ await syncSnippets(projectDir);
362
+ s.stop('✓ 代码片段同步完成');
363
+
364
+ cleanupTemplateFiles(projectDir);
365
+ s.stop('✓ 项目配置完成');
366
+
367
+ // 完成
368
+ p.outro(
369
+ chalk.green.bold('✨ 项目创建成功!\n\n') +
370
+ chalk.cyan('下一步:\n') +
371
+ chalk.yellow(` cd ${projectName}\n`) +
372
+ chalk.yellow(` flutter pub get\n`) +
373
+ chalk.yellow(` flutter run`)
374
+ );
375
+
376
+ // 记忆默认模板
377
+ if (templateSelection?.kind === 'builtin') {
378
+ saveDefaultTemplate({ type: 'builtin', idOrName: templateSelection.name });
379
+ } else if (templateSelection?.kind === 'custom') {
380
+ saveDefaultTemplate({ type: 'custom', idOrName: templateSelection.id });
381
+ }
382
+
383
+ // 正常退出
384
+ process.exit(0);
385
+ } catch (error) {
386
+ s.stop('创建失败');
387
+ throw error;
388
+ }
389
+ }
390
+
391
+ /**
392
+ * 验证项目名称
393
+ */
394
+ function isValidProjectName (name) {
395
+ return /^[a-z][a-z0-9_]*$/.test(name);
396
+ }
@@ -0,0 +1,39 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+
8
+ /**
9
+ * 同步代码片段到当前项目
10
+ */
11
+ export async function syncSnippets (targetDir) {
12
+ const projectRoot = targetDir || process.cwd();
13
+ const vscodeDir = path.join(projectRoot, '.vscode');
14
+ const targetFile = path.join(vscodeDir, 'dart.code-snippets');
15
+
16
+ // 获取 centralized snippets 路径
17
+ // 假设结构: packages/v2/lib/commands/snippets.js -> packages/v2/templates/snippets/dart.code-snippets
18
+ const snippetsSource = path.join(__dirname, '../../templates/snippets/dart.code-snippets');
19
+
20
+ if (!fs.existsSync(snippetsSource)) {
21
+ console.log(chalk.red('❌ 错误: 找不到标准代码片段源文件'));
22
+ return;
23
+ }
24
+
25
+ try {
26
+ // 确保 .vscode 目录存在
27
+ await fs.ensureDir(vscodeDir);
28
+
29
+ // 复制文件
30
+ await fs.copy(snippetsSource, targetFile, { overwrite: true });
31
+
32
+ console.log(chalk.green('✅ 代码片段同步成功!'));
33
+ console.log(chalk.gray(`已更新: ${targetFile}`));
34
+ console.log(chalk.cyan('提示: 重启 VS Code 或运行 "Developer: Reload Window" 以应用更改'));
35
+
36
+ } catch (error) {
37
+ console.log(chalk.red(`❌ 同步失败: ${error.message}`));
38
+ }
39
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * 模板查看命令
3
+ * 显示所有模板或指定模板的详细信息
4
+ */
5
+
6
+ import chalk from 'chalk';
7
+ import { getAllTemplates, getTemplate, isValidTemplate } from '../../config/templates.js';
8
+ import { logger } from '../utils/logger.js';
9
+
10
+ /**
11
+ * 列出所有模板
12
+ */
13
+ export function listTemplates () {
14
+ logger.title('📦 可用的项目模板');
15
+
16
+ const templates = getAllTemplates();
17
+
18
+ templates.forEach((template, index) => {
19
+ console.log(chalk.bold(`${index + 1}. ${template.displayName}`));
20
+ console.log(chalk.gray(` ${template.description}`));
21
+ console.log(chalk.gray(` 复杂度: ${'⭐'.repeat(template.complexity)}${'☆'.repeat(5 - template.complexity)}`));
22
+ console.log(chalk.gray(` 团队规模: ${template.teamSize}`));
23
+ console.log(chalk.gray(` 代码量: ${template.codeSize}`));
24
+ logger.newLine();
25
+ });
26
+
27
+ logger.info('使用 "flu-cli templates <name>" 查看详细信息');
28
+ logger.info('使用 "flu-cli new <project-name> -t <template>" 创建项目');
29
+
30
+ logger.newLine();
31
+ console.log(chalk.gray('示例:'));
32
+ console.log(chalk.yellow(' flu-cli templates lite'));
33
+ console.log(chalk.yellow(' flu-cli new my_app -t modular'));
34
+ }
35
+
36
+ /**
37
+ * 显示模板详情
38
+ */
39
+ export function showTemplateDetail (templateName) {
40
+ if (!isValidTemplate(templateName)) {
41
+ logger.error(`模板 "${templateName}" 不存在`);
42
+ logger.info('使用 "flu-cli templates" 查看所有可用模板');
43
+ return;
44
+ }
45
+
46
+ const template = getTemplate(templateName);
47
+
48
+ // 显示模板信息框
49
+ console.log(chalk.bold.cyan('\n┌─────────────────────────────────────────────────────────────┐'));
50
+ console.log(chalk.bold.cyan(`│ 📦 ${template.displayName.padEnd(56)} │`));
51
+ console.log(chalk.bold.cyan('├─────────────────────────────────────────────────────────────┤'));
52
+
53
+ // 基本信息
54
+ console.log(chalk.cyan('│ ') + chalk.gray('描述: ') + template.description.padEnd(50) + chalk.cyan(' │'));
55
+ console.log(chalk.cyan('│ ') + chalk.gray('复杂度: ') + ('⭐'.repeat(template.complexity) + '☆'.repeat(5 - template.complexity)).padEnd(50) + chalk.cyan(' │'));
56
+ console.log(chalk.cyan('│ ') + chalk.gray('团队规模: ') + template.teamSize.padEnd(48) + chalk.cyan(' │'));
57
+ console.log(chalk.cyan('│ ') + chalk.gray('代码量: ') + template.codeSize.padEnd(50) + chalk.cyan(' │'));
58
+
59
+ console.log(chalk.bold.cyan('├─────────────────────────────────────────────────────────────┤'));
60
+ console.log(chalk.bold.cyan('│ 特性: │'));
61
+ console.log(chalk.cyan('│ │'));
62
+
63
+ template.features.forEach(feature => {
64
+ console.log(chalk.cyan('│ ') + feature.padEnd(60) + chalk.cyan(' │'));
65
+ });
66
+
67
+ console.log(chalk.bold.cyan('├─────────────────────────────────────────────────────────────┤'));
68
+ console.log(chalk.bold.cyan('│ 项目结构: │'));
69
+ console.log(chalk.cyan('│ │'));
70
+
71
+ // 显示项目结构
72
+ const structureLines = template.structure.trim().split('\n');
73
+ structureLines.forEach(line => {
74
+ const paddedLine = line.padEnd(60);
75
+ console.log(chalk.cyan('│ ') + paddedLine + chalk.cyan(' │'));
76
+ });
77
+
78
+ console.log(chalk.bold.cyan('└─────────────────────────────────────────────────────────────┘'));
79
+
80
+ logger.newLine();
81
+ logger.info('创建项目:');
82
+ console.log(chalk.yellow(` flu-cli new my_app -t ${templateName}`));
83
+ logger.newLine();
84
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Component 生成器
3
+ */
4
+
5
+ import { existsSync, mkdirSync, writeFileSync } from 'fs';
6
+ import { join } from 'path';
7
+ import { logger } from '../utils/logger.js';
8
+ import { toPascalCase, toSnakeCase } from '../utils/string_helper.js';
9
+ import { updateIndexFile } from '../utils/index_updater.js';
10
+ import { getSnippetContent } from '../utils/snippet_loader.js';
11
+
12
+ /**
13
+ * 生成 Component 文件
14
+ */
15
+ export function generateComponent (name, options = {}) {
16
+ try {
17
+ const {
18
+ feature = null,
19
+ outputDir = process.cwd()
20
+ } = options;
21
+
22
+ // 转换命名
23
+ const namePascal = toPascalCase(name);
24
+ const nameSnake = toSnakeCase(name);
25
+
26
+ // 确定输出路径
27
+ let componentsDir;
28
+ if (feature) {
29
+ // Modular/Clean 架构
30
+ componentsDir = join(outputDir, 'lib', 'features', feature, 'components');
31
+ } else {
32
+ // Lite 架构
33
+ componentsDir = join(outputDir, 'lib', 'components');
34
+ }
35
+
36
+ // 创建目录
37
+ if (!existsSync(componentsDir)) {
38
+ mkdirSync(componentsDir, { recursive: true });
39
+ }
40
+
41
+ // 生成文件内容(优先片段)
42
+ const content = getSnippetContent(outputDir, 'flu.component', { Name: namePascal }) || generateComponentContent(namePascal);
43
+
44
+ // 写入文件
45
+ const filePath = join(componentsDir, `${nameSnake}_component.dart`);
46
+ if (existsSync(filePath)) {
47
+ logger.error(`文件已存在: ${filePath}`);
48
+ return false;
49
+ }
50
+
51
+ writeFileSync(filePath, content, 'utf8');
52
+ logger.success(`Component 创建成功: ${filePath}`);
53
+
54
+ // 更新 index.dart
55
+ updateIndexFile(componentsDir, `${nameSnake}_component.dart`);
56
+
57
+ return true;
58
+
59
+ } catch (error) {
60
+ logger.error(`生成 Component 失败: ${error.message}`);
61
+ return false;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * 生成 Component 内容
67
+ */
68
+ function generateComponentContent (namePascal) {
69
+ return `import 'package:flutter/material.dart';
70
+
71
+ class ${namePascal}Component extends StatelessWidget {
72
+ const ${namePascal}Component({super.key});
73
+
74
+ @override
75
+ Widget build(BuildContext context) {
76
+ return Container(
77
+ padding: const EdgeInsets.all(16),
78
+ child: Column(
79
+ crossAxisAlignment: CrossAxisAlignment.start,
80
+ children: [
81
+ Text(
82
+ '${namePascal}Component',
83
+ style: Theme.of(context).textTheme.titleLarge,
84
+ ),
85
+ const SizedBox(height: 8),
86
+ const Text('Component content goes here'),
87
+ ],
88
+ ),
89
+ );
90
+ }
91
+ }
92
+ `;
93
+ }