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
package/lib/commands/newClack.js
CHANGED
|
@@ -13,11 +13,11 @@ import { getTemplate, isValidTemplate, getAllTemplates } from '../../config/temp
|
|
|
13
13
|
import { selectTemplateWithEnquirer } from '../utils/templateSelectorEnquirer.js';
|
|
14
14
|
import { cloneOrUpdateTemplate } from '../templates/templateManager.js';
|
|
15
15
|
import { getAuthorName, saveAuthorName, saveDefaultTemplate } from '../utils/config.js';
|
|
16
|
-
import { ConfigManager } from '@flu-cli/core';
|
|
16
|
+
import { ConfigManager, ProjectGenerator } from '@flu-cli/core';
|
|
17
17
|
import { copyTemplate, replaceVariables, copyCustomTemplate, ensurePubspecName, mergePubspecFromTemplate, cleanupTemplateFiles } from '../templates/templateCopier.js';
|
|
18
18
|
import { runFlutterCreate } from '../utils/flutterHelper.js';
|
|
19
|
-
import { ProjectGenerator } from '../generators/project_generator.js';
|
|
20
19
|
import { syncSnippets } from './snippets.js';
|
|
20
|
+
import { configAssets } from './assets.js';
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* 创建新项目
|
|
@@ -52,19 +52,42 @@ export async function newProjectWithClack (projectName, options) {
|
|
|
52
52
|
// Step 2: 选择模板(实时预览,支持内置/自定义)
|
|
53
53
|
templateSelection = await selectTemplateWithEnquirer();
|
|
54
54
|
|
|
55
|
-
// Step 2.5: 选择状态管理器
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
55
|
+
// Step 2.5: 选择状态管理器 (Native 模板跳过)
|
|
56
|
+
let includeNetworkLayer = true; // 默认包含网络层
|
|
57
|
+
if (templateSelection.kind === 'builtin' && templateSelection.name === 'native') {
|
|
58
|
+
stateManager = 'default';
|
|
59
|
+
includeNetworkLayer = false; // Native 模板不需要网络层
|
|
60
|
+
} else {
|
|
61
|
+
const smChoice = await p.select({
|
|
62
|
+
message: '请选择状态管理器',
|
|
63
|
+
options: [
|
|
64
|
+
{ value: 'default', label: 'ChangeNotifier (默认)' },
|
|
65
|
+
{ value: 'provider', label: 'Provider' },
|
|
66
|
+
{ value: 'getx', label: 'GetX' }
|
|
67
|
+
// { value: 'riverpod', label: 'Riverpod' }
|
|
68
|
+
],
|
|
69
|
+
initialValue: 'default'
|
|
70
|
+
});
|
|
71
|
+
if (p.isCancel(smChoice)) {
|
|
72
|
+
p.cancel('操作已取消');
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
67
75
|
stateManager = smChoice;
|
|
76
|
+
|
|
77
|
+
// Step 2.6: 选择是否包含网络层
|
|
78
|
+
const nlChoice = await p.select({
|
|
79
|
+
message: '是否包含网络层?',
|
|
80
|
+
options: [
|
|
81
|
+
{ value: true, label: '包含网络层 (推荐)', hint: '包含 Dio 网络工具、AppConfig、StorageUtil' },
|
|
82
|
+
{ value: false, label: '不包含网络层', hint: '纯净项目,适合工具类或离线应用' }
|
|
83
|
+
],
|
|
84
|
+
initialValue: true
|
|
85
|
+
});
|
|
86
|
+
if (p.isCancel(nlChoice)) {
|
|
87
|
+
p.cancel('操作已取消');
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
includeNetworkLayer = nlChoice;
|
|
68
91
|
}
|
|
69
92
|
|
|
70
93
|
// Step 3: 项目路径(含冲突检测与删除确认)
|
|
@@ -158,6 +181,7 @@ export async function newProjectWithClack (projectName, options) {
|
|
|
158
181
|
console.log('');
|
|
159
182
|
console.log(` ${chalk.gray('项目名称:')} ${chalk.green(finalProjectName)}`);
|
|
160
183
|
console.log(` ${chalk.gray('项目模板:')} ${chalk.green(templateDisplay)}`);
|
|
184
|
+
console.log(` ${chalk.gray('网络层:')} ${chalk.green(includeNetworkLayer ? '包含' : '不包含')}`);
|
|
161
185
|
console.log(` ${chalk.gray('项目路径:')} ${chalk.green(projectPath)}`);
|
|
162
186
|
console.log(` ${chalk.gray('包名:')} ${chalk.green(packageName)}`);
|
|
163
187
|
console.log(` ${chalk.gray('作者:')} ${chalk.green(author)}`);
|
|
@@ -173,7 +197,7 @@ export async function newProjectWithClack (projectName, options) {
|
|
|
173
197
|
}
|
|
174
198
|
|
|
175
199
|
// Step 8: 创建项目
|
|
176
|
-
await createProject(finalProjectName, templateSelection, projectPath, { projectName: finalProjectName, packageName, author, stateManager }, options);
|
|
200
|
+
await createProject(finalProjectName, templateSelection, projectPath, { projectName: finalProjectName, packageName, author, stateManager, includeNetworkLayer }, options);
|
|
177
201
|
|
|
178
202
|
} else {
|
|
179
203
|
// ========== 命令行模式 ==========
|
|
@@ -251,7 +275,8 @@ export async function newProjectWithClack (projectName, options) {
|
|
|
251
275
|
}
|
|
252
276
|
|
|
253
277
|
// CLI 模式支持 --state 传参
|
|
254
|
-
|
|
278
|
+
const includeNetworkLayer = options.network !== false; // 默认包含,除非明确指定 --no-network
|
|
279
|
+
await createProject(finalProjectName, templateSelection, projectDir, { projectName: finalProjectName, packageName, author, stateManager, includeNetworkLayer }, options);
|
|
255
280
|
}
|
|
256
281
|
} catch (error) {
|
|
257
282
|
p.cancel(`创建失败: ${error.message}`);
|
|
@@ -266,106 +291,44 @@ async function createProject (projectName, templateSelection, projectDir, projec
|
|
|
266
291
|
const s = p.spinner();
|
|
267
292
|
|
|
268
293
|
try {
|
|
269
|
-
// 1.
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
);
|
|
291
|
-
} else if (templateSelection?.kind === 'custom') {
|
|
292
|
-
const configManager = ConfigManager.getInstance();
|
|
293
|
-
const ct = configManager.getTemplate(templateSelection.id);
|
|
294
|
-
|
|
295
|
-
if (!ct) {
|
|
296
|
-
s.stop('模板准备失败');
|
|
297
|
-
throw new Error('未找到自定义模板');
|
|
298
|
-
}
|
|
299
|
-
if (ct.type === 'local') {
|
|
300
|
-
templatePath = await cloneOrUpdateTemplate(
|
|
301
|
-
ct.id,
|
|
302
|
-
null,
|
|
303
|
-
'main',
|
|
304
|
-
!options.cache,
|
|
305
|
-
true,
|
|
306
|
-
ct.path || ct.url // 兼容旧配置
|
|
307
|
-
);
|
|
308
|
-
} else {
|
|
309
|
-
templatePath = await cloneOrUpdateTemplate(
|
|
310
|
-
ct.id,
|
|
311
|
-
ct.url,
|
|
312
|
-
ct.branch || 'main',
|
|
313
|
-
!options.cache,
|
|
314
|
-
false
|
|
315
|
-
);
|
|
316
|
-
}
|
|
317
|
-
} else {
|
|
318
|
-
throw new Error('模板选择无效');
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
if (!templatePath) {
|
|
322
|
-
s.stop('模板准备失败');
|
|
323
|
-
throw new Error('模板准备失败');
|
|
324
|
-
}
|
|
325
|
-
s.stop('✓ 模板准备完成');
|
|
326
|
-
|
|
327
|
-
// 2. 创建 Flutter 项目
|
|
328
|
-
s.start('正在创建 Flutter 项目...');
|
|
329
|
-
const created = await runFlutterCreate(projectDir, projectInfo.projectName, projectInfo.packageName);
|
|
330
|
-
if (created) {
|
|
331
|
-
s.stop('✓ Flutter 项目创建成功');
|
|
332
|
-
} else {
|
|
333
|
-
s.stop('Flutter 项目创建失败');
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// 3. 复制模板文件(自定义模板仅覆盖通用代码,保留平台与包名)
|
|
337
|
-
s.start('正在复制模板文件...');
|
|
338
|
-
const isCustom = templateSelection?.kind === 'custom';
|
|
339
|
-
const copied = isCustom
|
|
340
|
-
? await copyCustomTemplate(templatePath, projectDir)
|
|
341
|
-
: await copyTemplate(templatePath, projectDir);
|
|
294
|
+
// 1. 设置生成选项
|
|
295
|
+
const templateId = templateSelection.kind === 'builtin'
|
|
296
|
+
? templateSelection.name
|
|
297
|
+
: templateSelection.id;
|
|
298
|
+
|
|
299
|
+
const generatorOptions = {
|
|
300
|
+
templateType: templateId,
|
|
301
|
+
stateManager: projectInfo.stateManager || 'default',
|
|
302
|
+
packageName: projectInfo.packageName,
|
|
303
|
+
outputDir: dirname(projectDir),
|
|
304
|
+
createFlutterProject: true, // v2 总是需要 flutter create
|
|
305
|
+
forceUpdate: !options.cache,
|
|
306
|
+
author: projectInfo.author,
|
|
307
|
+
flutterTemplate: options.flutterTemplate || 'app',
|
|
308
|
+
includeNetworkLayer: projectInfo.includeNetworkLayer !== false // 默认包含网络层
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
// 2. 调用核心生成器
|
|
312
|
+
s.start('正在生成项目 (Core Engine)...');
|
|
313
|
+
const pg = new ProjectGenerator();
|
|
314
|
+
const success = await pg.generate(projectName, generatorOptions);
|
|
342
315
|
|
|
343
|
-
if (!
|
|
344
|
-
s.stop('
|
|
345
|
-
throw new Error('
|
|
316
|
+
if (!success) {
|
|
317
|
+
s.stop('项目生成失败');
|
|
318
|
+
throw new Error('核心生成器返回失败');
|
|
346
319
|
}
|
|
347
|
-
s.stop('✓
|
|
348
|
-
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
mergePubspecFromTemplate(templatePath, projectDir, projectInfo.projectName);
|
|
356
|
-
} else {
|
|
357
|
-
ensurePubspecName(projectDir, projectInfo.projectName);
|
|
320
|
+
s.stop('✓ 项目生成成功');
|
|
321
|
+
|
|
322
|
+
// 3. CLI 特色功能:同步标准代码片段 (Native 模板跳过)
|
|
323
|
+
const isNative = templateId === 'native' || templateId === 'none';
|
|
324
|
+
if (!isNative) {
|
|
325
|
+
s.start('正在同步代码片段...');
|
|
326
|
+
await syncSnippets(projectDir);
|
|
327
|
+
s.stop('✓ 代码片段同步完成');
|
|
358
328
|
}
|
|
359
|
-
// 配置状态管理器
|
|
360
|
-
await pg.configureStateManager(projectDir, projectInfo.stateManager || 'default');
|
|
361
|
-
|
|
362
|
-
// 同步标准代码片段
|
|
363
|
-
s.start('正在同步代码片段...');
|
|
364
|
-
await syncSnippets(projectDir);
|
|
365
|
-
s.stop('✓ 代码片段同步完成');
|
|
366
329
|
|
|
330
|
+
// 4. 清理模板残余(ProjectGenerator 可能已经做了一部分,这里确保万一)
|
|
367
331
|
cleanupTemplateFiles(projectDir);
|
|
368
|
-
s.stop('✓ 项目配置完成');
|
|
369
332
|
|
|
370
333
|
// 完成
|
|
371
334
|
p.outro(
|
|
@@ -376,7 +339,17 @@ async function createProject (projectName, templateSelection, projectDir, projec
|
|
|
376
339
|
chalk.yellow(` flutter run`)
|
|
377
340
|
);
|
|
378
341
|
|
|
379
|
-
//
|
|
342
|
+
// 5. 提示资源配置
|
|
343
|
+
const shouldConfigAssets = await p.confirm({
|
|
344
|
+
message: '项目已创建。是否现在就配置 App 图标和启动图?',
|
|
345
|
+
initialValue: false
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
if (shouldConfigAssets) {
|
|
349
|
+
await configAssets({ dir: projectDir });
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// 6. 记忆默认模板
|
|
380
353
|
if (templateSelection?.kind === 'builtin') {
|
|
381
354
|
saveDefaultTemplate({ type: 'builtin', idOrName: templateSelection.name });
|
|
382
355
|
} else if (templateSelection?.kind === 'custom') {
|
package/lib/commands/snippets.js
CHANGED
|
@@ -2,8 +2,8 @@ import fs from 'fs-extra';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import { ProjectConfigManager, getTemplatesRootDir } from '@flu-cli/core';
|
|
6
|
+
import { t } from '../utils/i18n.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* 同步代码片段到当前项目
|
|
@@ -13,23 +13,64 @@ export async function syncSnippets (targetDir) {
|
|
|
13
13
|
const vscodeDir = path.join(projectRoot, '.vscode');
|
|
14
14
|
const targetFile = path.join(vscodeDir, 'dart.code-snippets');
|
|
15
15
|
|
|
16
|
-
// 获取 centralized snippets 路径
|
|
17
|
-
|
|
18
|
-
const snippetsSource = path.join(
|
|
16
|
+
// 获取 centralized snippets 路径 (从 Core 的模板根目录获取)
|
|
17
|
+
const templatesDir = getTemplatesRootDir();
|
|
18
|
+
const snippetsSource = path.join(templatesDir, 'snippets/dart.code-snippets');
|
|
19
19
|
|
|
20
20
|
if (!fs.existsSync(snippetsSource)) {
|
|
21
|
-
console.log(chalk.red(
|
|
21
|
+
console.log(chalk.red(`❌ 错误: 找不到标准代码片段源文件: ${snippetsSource}`));
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
try {
|
|
26
|
+
// 读取原始 Snippets
|
|
27
|
+
const originalSnippets = await fs.readJson(snippetsSource);
|
|
28
|
+
let finalSnippets = { ...originalSnippets };
|
|
29
|
+
|
|
30
|
+
// 读取项目配置
|
|
31
|
+
const config = ProjectConfigManager.loadConfig(projectRoot);
|
|
32
|
+
|
|
33
|
+
if (config) {
|
|
34
|
+
console.log(chalk.cyan(t('snippets.detect_config')));
|
|
35
|
+
const pageConfig = config.generators?.page;
|
|
36
|
+
const vmConfig = config.generators?.viewModel;
|
|
37
|
+
|
|
38
|
+
const withBasePage = pageConfig?.withBasePage ?? true;
|
|
39
|
+
const withViewModel = pageConfig?.withViewModel ?? true;
|
|
40
|
+
const withBaseViewModel = vmConfig?.withBaseViewModel ?? true;
|
|
41
|
+
|
|
42
|
+
// 过滤规则
|
|
43
|
+
if (!withBasePage) {
|
|
44
|
+
delete finalSnippets['flu.stPage'];
|
|
45
|
+
delete finalSnippets['flu.listPage'];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!withViewModel) {
|
|
49
|
+
delete finalSnippets['flu.stPage'];
|
|
50
|
+
delete finalSnippets['flu.viewmodel'];
|
|
51
|
+
delete finalSnippets['flu.listPage'];
|
|
52
|
+
delete finalSnippets['flu.listViewModel'];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!withBaseViewModel) {
|
|
56
|
+
delete finalSnippets['flu.viewmodel'];
|
|
57
|
+
delete finalSnippets['flu.listViewModel'];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
26
61
|
// 确保 .vscode 目录存在
|
|
27
62
|
await fs.ensureDir(vscodeDir);
|
|
28
63
|
|
|
29
|
-
//
|
|
30
|
-
await fs.
|
|
64
|
+
// 写入文件
|
|
65
|
+
await fs.writeJson(targetFile, finalSnippets, { spaces: 2 });
|
|
31
66
|
|
|
32
|
-
console.log(chalk.green('
|
|
67
|
+
console.log(chalk.green(t('snippets.sync_success')));
|
|
68
|
+
if (config) {
|
|
69
|
+
console.log(chalk.gray(t('snippets.applied_filter', {
|
|
70
|
+
basePage: config.generators?.page?.withBasePage ? 'ON' : 'OFF',
|
|
71
|
+
viewModel: config.generators?.page?.withViewModel ? 'ON' : 'OFF'
|
|
72
|
+
})));
|
|
73
|
+
}
|
|
33
74
|
console.log(chalk.gray(`已更新: ${targetFile}`));
|
|
34
75
|
console.log(chalk.cyan('提示: 重启 VS Code 或运行 "Developer: Reload Window" 以应用更改'));
|
|
35
76
|
|
|
@@ -1,108 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 模板复制器
|
|
3
|
-
* 职责:复制内置/自定义模板至目标目录,并执行变量替换与文件筛选
|
|
4
|
-
* 特性:保留平台工程与包名、支持 .template 文件落地、清理模板特有文件
|
|
2
|
+
* 模板复制器 - 桥接至 @flu-cli/core
|
|
5
3
|
*/
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
export {
|
|
6
|
+
copyTemplate,
|
|
7
|
+
copyCustomTemplate,
|
|
8
|
+
replaceVariables,
|
|
9
|
+
ensurePubspecName,
|
|
10
|
+
cleanupTemplateFiles
|
|
11
|
+
} from '@flu-cli/core';
|
|
13
12
|
|
|
14
13
|
/**
|
|
15
|
-
*
|
|
16
|
-
* @param {string} templatePath - 模板路径
|
|
17
|
-
* @param {string} targetPath - 目标路径
|
|
18
|
-
* @returns {Promise<boolean>} 是否成功
|
|
14
|
+
* 使用模板的 pubspec.yaml 合并覆盖目标项目 (仅限 V2 保持逻辑)
|
|
19
15
|
*/
|
|
20
|
-
export async function
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (!existsSync(templatePath)) {
|
|
24
|
-
logger.error(`模板路径不存在: ${templatePath}`);
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// 需要排除的文件和目录
|
|
29
|
-
const excludes = [
|
|
30
|
-
'.git',
|
|
31
|
-
'.github',
|
|
32
|
-
'node_modules',
|
|
33
|
-
'.flu-cli.yaml',
|
|
34
|
-
'README.template.md',
|
|
35
|
-
'.DS_Store',
|
|
36
|
-
'pubspec.yaml' // 排除本地调试用的 pubspec.yaml
|
|
37
|
-
];
|
|
38
|
-
|
|
39
|
-
// 复制文件,排除特定文件
|
|
40
|
-
copySync(templatePath, targetPath, {
|
|
41
|
-
filter: (src) => {
|
|
42
|
-
const basename = src.split('/').pop();
|
|
43
|
-
return !excludes.includes(basename);
|
|
44
|
-
},
|
|
45
|
-
overwrite: true
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
// 处理 pubspec.yaml.template -> pubspec.yaml
|
|
49
|
-
const templatePubspec = join(templatePath, 'pubspec.yaml.template');
|
|
50
|
-
const targetPubspec = join(targetPath, 'pubspec.yaml');
|
|
51
|
-
if (existsSync(templatePubspec)) {
|
|
52
|
-
// 复制 .template 文件并覆盖 Flutter 生成的 pubspec.yaml
|
|
53
|
-
copySync(templatePubspec, targetPubspec, { overwrite: true });
|
|
54
|
-
logger.info('已使用模板的 pubspec.yaml.template');
|
|
55
|
-
} else {
|
|
56
|
-
logger.warn('未找到 pubspec.yaml.template,使用 Flutter 默认配置');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return true;
|
|
60
|
-
|
|
61
|
-
} catch (error) {
|
|
62
|
-
logger.error(`复制模板失败: ${error.message}`);
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// 仅复制自定义模板中的通用代码与配置,保留目标工程的平台与包名
|
|
68
|
-
export async function copyCustomTemplate (templatePath, targetPath) {
|
|
69
|
-
try {
|
|
70
|
-
if (!existsSync(templatePath)) {
|
|
71
|
-
logger.error(`模板路径不存在: ${templatePath}`);
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const includeDirs = ['lib', 'assets', '.vscode'];
|
|
76
|
-
const includeFiles = ['analysis_options.yaml', 'README.md', 'pubspec.yaml'];
|
|
77
|
-
|
|
78
|
-
for (const dir of includeDirs) {
|
|
79
|
-
const srcDir = join(templatePath, dir);
|
|
80
|
-
const dstDir = join(targetPath, dir);
|
|
81
|
-
if (existsSync(srcDir)) {
|
|
82
|
-
copySync(srcDir, dstDir, { overwrite: true });
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
for (const file of includeFiles) {
|
|
87
|
-
const srcFile = join(templatePath, file);
|
|
88
|
-
const dstFile = join(targetPath, file);
|
|
89
|
-
if (existsSync(srcFile)) {
|
|
90
|
-
copySync(srcFile, dstFile, { overwrite: true });
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return true;
|
|
95
|
-
} catch (error) {
|
|
96
|
-
logger.error(`复制自定义模板失败: ${error.message}`);
|
|
97
|
-
return false;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* 使用模板的 pubspec.yaml 合并覆盖目标项目(仅修改 name 保持项目名)
|
|
103
|
-
*/
|
|
104
|
-
export function mergePubspecFromTemplate (templatePath, projectDir, projectName) {
|
|
16
|
+
export async function mergePubspecFromTemplate (templatePath, projectDir, projectName) {
|
|
17
|
+
// 暂时保留该逻辑或移动到 core (考虑到 core 主要是 ProjectGenerator 使用,可以先在 v2 桥接)
|
|
18
|
+
// TODO: 后续可以完全迁移到 core
|
|
105
19
|
try {
|
|
20
|
+
const { readFileSync, writeFileSync, existsSync } = await import('fs');
|
|
21
|
+
const { join } = await import('path');
|
|
106
22
|
const tplPub = join(templatePath, 'pubspec.yaml');
|
|
107
23
|
const dstPub = join(projectDir, 'pubspec.yaml');
|
|
108
24
|
if (!existsSync(tplPub) || !existsSync(dstPub)) return;
|
|
@@ -117,180 +33,3 @@ export function mergePubspecFromTemplate (templatePath, projectDir, projectName)
|
|
|
117
33
|
// 忽略错误
|
|
118
34
|
}
|
|
119
35
|
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* 替换文件中的变量
|
|
123
|
-
* @param {string} projectDir - 项目目录
|
|
124
|
-
* @param {object} variables - 变量对象
|
|
125
|
-
*/
|
|
126
|
-
export async function replaceVariables (projectDir, variables) {
|
|
127
|
-
try {
|
|
128
|
-
// 需要替换变量的文件模式
|
|
129
|
-
const patterns = [
|
|
130
|
-
'**/*.dart',
|
|
131
|
-
'pubspec.yaml',
|
|
132
|
-
'README.md',
|
|
133
|
-
'android/app/build.gradle',
|
|
134
|
-
'ios/Runner/Info.plist'
|
|
135
|
-
];
|
|
136
|
-
|
|
137
|
-
// 变量映射
|
|
138
|
-
const replacements = {
|
|
139
|
-
'{{projectName}}': variables.projectName,
|
|
140
|
-
'{{project_name}}': variables.projectName,
|
|
141
|
-
'{{package_name}}': variables.packageName,
|
|
142
|
-
'{{author}}': variables.author || 'Your Name',
|
|
143
|
-
'{{year}}': new Date().getFullYear().toString()
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
// 递归替换目录中的文件
|
|
147
|
-
replaceInDirectory(projectDir, replacements);
|
|
148
|
-
|
|
149
|
-
// 修复 test 文件中的导入路径
|
|
150
|
-
fixTestFile(projectDir, variables.projectName);
|
|
151
|
-
|
|
152
|
-
return true;
|
|
153
|
-
|
|
154
|
-
} catch (error) {
|
|
155
|
-
logger.error(`替换变量失败: ${error.message}`);
|
|
156
|
-
return false;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* 递归替换目录中的文件内容
|
|
162
|
-
*/
|
|
163
|
-
function replaceInDirectory (dir, replacements) {
|
|
164
|
-
const files = readdirSync(dir);
|
|
165
|
-
|
|
166
|
-
files.forEach(file => {
|
|
167
|
-
const filePath = join(dir, file);
|
|
168
|
-
const stat = statSync(filePath);
|
|
169
|
-
|
|
170
|
-
if (stat.isDirectory()) {
|
|
171
|
-
// 跳过特定目录
|
|
172
|
-
if (['.git', 'node_modules', '.dart_tool', 'build'].includes(file)) {
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
replaceInDirectory(filePath, replacements);
|
|
176
|
-
} else if (stat.isFile()) {
|
|
177
|
-
// 只处理文本文件
|
|
178
|
-
if (isTextFile(filePath)) {
|
|
179
|
-
replaceInFile(filePath, replacements);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* 替换单个文件中的内容
|
|
187
|
-
*/
|
|
188
|
-
function replaceInFile (filePath, replacements) {
|
|
189
|
-
try {
|
|
190
|
-
let content = readFileSync(filePath, 'utf8');
|
|
191
|
-
let modified = false;
|
|
192
|
-
|
|
193
|
-
// 执行所有替换
|
|
194
|
-
Object.entries(replacements).forEach(([pattern, value]) => {
|
|
195
|
-
if (content.includes(pattern)) {
|
|
196
|
-
content = content.replace(new RegExp(pattern, 'g'), value);
|
|
197
|
-
modified = true;
|
|
198
|
-
}
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
// 如果有修改,写回文件
|
|
202
|
-
if (modified) {
|
|
203
|
-
writeFileSync(filePath, content, 'utf8');
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
} catch (error) {
|
|
207
|
-
// 忽略二进制文件等错误
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* 判断是否为文本文件
|
|
213
|
-
*/
|
|
214
|
-
function isTextFile (filePath) {
|
|
215
|
-
const textExtensions = [
|
|
216
|
-
'.dart', '.yaml', '.yml', '.json', '.md', '.txt',
|
|
217
|
-
'.gradle', '.xml', '.plist', '.swift', '.kt', '.java',
|
|
218
|
-
'.js', '.ts', '.html', '.css', '.sh'
|
|
219
|
-
];
|
|
220
|
-
|
|
221
|
-
return textExtensions.some(ext => filePath.endsWith(ext));
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* 修复 test 文件中的导入路径
|
|
226
|
-
*/
|
|
227
|
-
function fixTestFile (projectDir, projectName) {
|
|
228
|
-
const testFile = join(projectDir, 'test', 'widget_test.dart');
|
|
229
|
-
|
|
230
|
-
if (existsSync(testFile)) {
|
|
231
|
-
try {
|
|
232
|
-
let content = readFileSync(testFile, 'utf8');
|
|
233
|
-
|
|
234
|
-
// 替换导入路径:从 main.dart 改为 app.dart
|
|
235
|
-
content = content.replace(
|
|
236
|
-
`import 'package:${projectName}/main.dart';`,
|
|
237
|
-
`import 'package:${projectName}/app.dart';`
|
|
238
|
-
);
|
|
239
|
-
|
|
240
|
-
// 如果测试内容还是默认的计数器测试,可以注释掉或删除
|
|
241
|
-
if (content.includes('Counter increments smoke test')) {
|
|
242
|
-
content = `import 'package:flutter_test/flutter_test.dart';
|
|
243
|
-
import 'package:${projectName}/app.dart';
|
|
244
|
-
|
|
245
|
-
void main() {
|
|
246
|
-
testWidgets('App smoke test', (WidgetTester tester) async {
|
|
247
|
-
await tester.pumpWidget(const App());
|
|
248
|
-
expect(find.byType(App), findsOneWidget);
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
`;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
writeFileSync(testFile, content, 'utf8');
|
|
255
|
-
} catch (error) {
|
|
256
|
-
// 忽略错误
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* 强制校正 pubspec.yaml 的 name 字段为项目名
|
|
263
|
-
*/
|
|
264
|
-
export function ensurePubspecName (projectDir, projectName) {
|
|
265
|
-
try {
|
|
266
|
-
const pubspec = join(projectDir, 'pubspec.yaml');
|
|
267
|
-
if (!existsSync(pubspec)) return;
|
|
268
|
-
let content = readFileSync(pubspec, 'utf8');
|
|
269
|
-
if (/^name:\s+/m.test(content)) {
|
|
270
|
-
content = content.replace(/^name:\s+.*/m, `name: ${projectName}`);
|
|
271
|
-
} else {
|
|
272
|
-
content = `name: ${projectName}\n` + content;
|
|
273
|
-
}
|
|
274
|
-
writeFileSync(pubspec, content, 'utf8');
|
|
275
|
-
} catch (error) {
|
|
276
|
-
// 忽略错误
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* 清理模板特有文件
|
|
282
|
-
*/
|
|
283
|
-
export function cleanupTemplateFiles (projectDir) {
|
|
284
|
-
const filesToRemove = [
|
|
285
|
-
'.flu-cli.yaml',
|
|
286
|
-
'README.template.md',
|
|
287
|
-
'.git'
|
|
288
|
-
];
|
|
289
|
-
|
|
290
|
-
filesToRemove.forEach(file => {
|
|
291
|
-
const filePath = join(projectDir, file);
|
|
292
|
-
if (existsSync(filePath)) {
|
|
293
|
-
removeSync(filePath);
|
|
294
|
-
}
|
|
295
|
-
});
|
|
296
|
-
}
|