flu-cli 2.0.5 → 2.1.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/CHANGELOG.md +23 -0
- package/README.md +17 -4
- package/config/dev.config.js +11 -11
- package/config/templates.js +10 -10
- package/index.js +554 -102
- package/lib/commands/add.js +365 -266
- package/lib/commands/assets.js +77 -78
- package/lib/commands/cache.js +29 -52
- package/lib/commands/completion.js +13 -11
- package/lib/commands/config.js +150 -44
- package/lib/commands/init-ai-base.js +89 -0
- package/lib/commands/newClack.js +269 -178
- package/lib/commands/snippets.js +58 -43
- package/lib/commands/template.js +98 -58
- package/lib/commands/templates.js +101 -57
- package/lib/commands/upload.js +313 -0
- package/lib/commands/vnext-options.js +206 -0
- package/lib/generators/model_generator.js +91 -88
- package/lib/generators/page_generator.js +100 -93
- package/lib/generators/service_generator.js +44 -39
- package/lib/generators/viewmodel_generator.js +25 -29
- package/lib/generators/widget_generator.js +30 -35
- package/lib/templates/templateCopier.js +14 -15
- package/lib/templates/templateManager.js +22 -21
- package/lib/utils/config.js +37 -20
- package/lib/utils/flutterHelper.js +2 -2
- package/lib/utils/i18n.js +3 -3
- package/lib/utils/index_updater.js +22 -23
- package/lib/utils/json-output.js +59 -0
- package/lib/utils/logger.js +17 -17
- package/lib/utils/project_detector.js +66 -66
- package/lib/utils/snippet_loader.js +21 -19
- package/lib/utils/string_helper.js +13 -13
- package/lib/utils/templateSelectorEnquirer.js +94 -108
- package/locales/en-US.json +1 -1
- package/locales/zh-CN.json +2 -2
- package/package.json +60 -57
- package/scripts/smoke-vnext-generate.mjs +1934 -0
- package/scripts/smoke-vnext-params.mjs +92 -0
- package/CLI.md +0 -513
- package/release.sh +0 -529
- package/scripts/e2e-state-tests.js +0 -116
- package/scripts/sync-base-to-templates.js +0 -108
- package/scripts/workspace-clone-all.sh +0 -101
- package/scripts/workspace-status-all.sh +0 -112
package/lib/commands/newClack.js
CHANGED
|
@@ -4,369 +4,460 @@
|
|
|
4
4
|
* 说明:保留平台与包名,模板文件覆盖通用代码;支持内置/自定义模板与缓存开关
|
|
5
5
|
*/
|
|
6
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, getAllTemplates } from '../../config/templates.js'
|
|
13
|
-
import { selectTemplateWithEnquirer } from '../utils/templateSelectorEnquirer.js'
|
|
14
|
-
import { cloneOrUpdateTemplate } from '../templates/templateManager.js'
|
|
15
|
-
import { getAuthorName, saveAuthorName, saveDefaultTemplate } from '../utils/config.js'
|
|
16
|
-
import { ConfigManager, ProjectGenerator } from 'flu-cli-core'
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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, getAllTemplates } from '../../config/templates.js'
|
|
13
|
+
import { selectTemplateWithEnquirer } from '../utils/templateSelectorEnquirer.js'
|
|
14
|
+
import { cloneOrUpdateTemplate } from '../templates/templateManager.js'
|
|
15
|
+
import { getAuthorName, saveAuthorName, saveDefaultTemplate } from '../utils/config.js'
|
|
16
|
+
import { ConfigManager, ProjectGenerator } from 'flu-cli-core'
|
|
17
|
+
import {
|
|
18
|
+
copyTemplate,
|
|
19
|
+
replaceVariables,
|
|
20
|
+
copyCustomTemplate,
|
|
21
|
+
ensurePubspecName,
|
|
22
|
+
mergePubspecFromTemplate,
|
|
23
|
+
cleanupTemplateFiles,
|
|
24
|
+
} from '../templates/templateCopier.js'
|
|
25
|
+
import { runFlutterCreate } from '../utils/flutterHelper.js'
|
|
26
|
+
import { syncSnippets } from './snippets.js'
|
|
27
|
+
import { configAssets } from './assets.js'
|
|
28
|
+
import { buildVNextProjectOptions } from './vnext-options.js'
|
|
21
29
|
|
|
22
30
|
/**
|
|
23
31
|
* 创建新项目
|
|
24
32
|
*/
|
|
25
|
-
export async function newProjectWithClack
|
|
26
|
-
|
|
33
|
+
export async function newProjectWithClack(projectName, options) {
|
|
34
|
+
const jsonMode = options?.json === true
|
|
35
|
+
if (jsonMode) {
|
|
36
|
+
process.env.FLU_CLI_NON_INTERACTIVE = '1'
|
|
37
|
+
process.env.FLU_CLI_JSON_OUTPUT = '1'
|
|
38
|
+
if (!projectName) throw new Error('json 模式下必须提供 project-name')
|
|
39
|
+
if (!options.template) throw new Error('json 模式下必须提供 --template <type>')
|
|
40
|
+
} else {
|
|
41
|
+
p.intro(chalk.cyan.bold('🚀 创建 Flutter 项目'))
|
|
42
|
+
}
|
|
27
43
|
|
|
28
44
|
try {
|
|
29
|
-
const isInteractive = !projectName
|
|
30
|
-
let finalProjectName = projectName
|
|
31
|
-
let templateSelection = null
|
|
32
|
-
let stateManager = options.state || 'default'
|
|
33
|
-
const nonInteractive = process.env.FLU_CLI_NON_INTERACTIVE === '1'
|
|
45
|
+
const isInteractive = !projectName
|
|
46
|
+
let finalProjectName = projectName
|
|
47
|
+
let templateSelection = null
|
|
48
|
+
let stateManager = options.state || 'default'
|
|
49
|
+
const nonInteractive = process.env.FLU_CLI_NON_INTERACTIVE === '1'
|
|
34
50
|
|
|
35
51
|
// ========== 交互式模式 ==========
|
|
36
52
|
if (isInteractive) {
|
|
37
53
|
// Step 1: 项目名称
|
|
38
54
|
finalProjectName = await p.text({
|
|
39
55
|
message: '请输入项目名称(直接回车使用: my_app)',
|
|
40
|
-
placeholder: 'my_app'
|
|
41
|
-
})
|
|
56
|
+
placeholder: 'my_app',
|
|
57
|
+
})
|
|
42
58
|
|
|
43
59
|
if (p.isCancel(finalProjectName)) {
|
|
44
|
-
p.cancel('操作已取消')
|
|
45
|
-
process.exit(0)
|
|
60
|
+
p.cancel('操作已取消')
|
|
61
|
+
process.exit(0)
|
|
46
62
|
}
|
|
47
63
|
|
|
48
64
|
if (!finalProjectName || finalProjectName === '') {
|
|
49
|
-
finalProjectName = 'my_app'
|
|
65
|
+
finalProjectName = 'my_app'
|
|
50
66
|
}
|
|
51
67
|
|
|
52
68
|
// Step 2: 选择模板(实时预览,支持内置/自定义)
|
|
53
|
-
templateSelection = await selectTemplateWithEnquirer()
|
|
69
|
+
templateSelection = await selectTemplateWithEnquirer()
|
|
54
70
|
|
|
55
71
|
// Step 2.5: 选择状态管理器 (Native 模板跳过)
|
|
56
|
-
let includeNetworkLayer = true
|
|
72
|
+
let includeNetworkLayer = true // 默认包含网络层
|
|
57
73
|
if (templateSelection.kind === 'builtin' && templateSelection.name === 'native') {
|
|
58
|
-
stateManager = 'default'
|
|
59
|
-
includeNetworkLayer = false
|
|
74
|
+
stateManager = 'default'
|
|
75
|
+
includeNetworkLayer = false // Native 模板不需要网络层
|
|
60
76
|
} else {
|
|
61
77
|
const smChoice = await p.select({
|
|
62
78
|
message: '请选择状态管理器',
|
|
63
79
|
options: [
|
|
64
80
|
{ value: 'default', label: 'ChangeNotifier (默认)' },
|
|
65
81
|
{ value: 'provider', label: 'Provider' },
|
|
66
|
-
{ value: 'getx', label: 'GetX' }
|
|
82
|
+
{ value: 'getx', label: 'GetX' },
|
|
67
83
|
// { value: 'riverpod', label: 'Riverpod' }
|
|
68
84
|
],
|
|
69
|
-
initialValue: 'default'
|
|
70
|
-
})
|
|
85
|
+
initialValue: 'default',
|
|
86
|
+
})
|
|
71
87
|
if (p.isCancel(smChoice)) {
|
|
72
|
-
p.cancel('操作已取消')
|
|
73
|
-
process.exit(0)
|
|
88
|
+
p.cancel('操作已取消')
|
|
89
|
+
process.exit(0)
|
|
74
90
|
}
|
|
75
|
-
stateManager = smChoice
|
|
91
|
+
stateManager = smChoice
|
|
76
92
|
|
|
77
93
|
// Step 2.6: 选择是否包含网络层
|
|
78
94
|
const nlChoice = await p.select({
|
|
79
95
|
message: '是否包含网络层?',
|
|
80
96
|
options: [
|
|
81
|
-
{
|
|
82
|
-
|
|
97
|
+
{
|
|
98
|
+
value: true,
|
|
99
|
+
label: '包含网络层 (推荐)',
|
|
100
|
+
hint: '包含 Dio 网络工具、AppConfig、StorageUtil',
|
|
101
|
+
},
|
|
102
|
+
{ value: false, label: '不包含网络层', hint: '纯净项目,适合工具类或离线应用' },
|
|
83
103
|
],
|
|
84
|
-
initialValue: true
|
|
85
|
-
})
|
|
104
|
+
initialValue: true,
|
|
105
|
+
})
|
|
86
106
|
if (p.isCancel(nlChoice)) {
|
|
87
|
-
p.cancel('操作已取消')
|
|
88
|
-
process.exit(0)
|
|
107
|
+
p.cancel('操作已取消')
|
|
108
|
+
process.exit(0)
|
|
89
109
|
}
|
|
90
|
-
includeNetworkLayer = nlChoice
|
|
110
|
+
includeNetworkLayer = nlChoice
|
|
91
111
|
}
|
|
92
112
|
|
|
93
113
|
// Step 3: 项目路径(含冲突检测与删除确认)
|
|
94
|
-
const defaultPath = join(options.dir, finalProjectName)
|
|
114
|
+
const defaultPath = join(options.dir, finalProjectName)
|
|
95
115
|
let projectPath = await p.text({
|
|
96
116
|
message: `请输入项目路径(直接回车使用: ${defaultPath})`,
|
|
97
|
-
placeholder: defaultPath
|
|
98
|
-
})
|
|
117
|
+
placeholder: defaultPath,
|
|
118
|
+
})
|
|
99
119
|
|
|
100
120
|
if (p.isCancel(projectPath)) {
|
|
101
|
-
p.cancel('操作已取消')
|
|
102
|
-
process.exit(0)
|
|
121
|
+
p.cancel('操作已取消')
|
|
122
|
+
process.exit(0)
|
|
103
123
|
}
|
|
104
124
|
|
|
105
125
|
if (!projectPath || projectPath === '') {
|
|
106
|
-
projectPath = defaultPath
|
|
126
|
+
projectPath = defaultPath
|
|
107
127
|
}
|
|
108
128
|
|
|
109
129
|
// Step 4: 包名
|
|
110
|
-
const defaultPackageName = `com.example.${finalProjectName}
|
|
130
|
+
const defaultPackageName = `com.example.${finalProjectName}`
|
|
111
131
|
let packageName = await p.text({
|
|
112
132
|
message: `请输入包名(直接回车使用: ${defaultPackageName})`,
|
|
113
|
-
placeholder: defaultPackageName
|
|
114
|
-
})
|
|
133
|
+
placeholder: defaultPackageName,
|
|
134
|
+
})
|
|
115
135
|
|
|
116
136
|
if (p.isCancel(packageName)) {
|
|
117
|
-
p.cancel('操作已取消')
|
|
118
|
-
process.exit(0)
|
|
137
|
+
p.cancel('操作已取消')
|
|
138
|
+
process.exit(0)
|
|
119
139
|
}
|
|
120
140
|
|
|
121
141
|
if (!packageName || packageName === '') {
|
|
122
|
-
packageName = defaultPackageName
|
|
142
|
+
packageName = defaultPackageName
|
|
123
143
|
}
|
|
124
144
|
|
|
125
145
|
// Step 5: 作者(使用缓存的名字)
|
|
126
|
-
const cachedAuthor = getAuthorName()
|
|
146
|
+
const cachedAuthor = getAuthorName()
|
|
127
147
|
let author = await p.text({
|
|
128
148
|
message: `请输入作者名称(直接回车使用: ${cachedAuthor})`,
|
|
129
|
-
placeholder: cachedAuthor
|
|
130
|
-
})
|
|
149
|
+
placeholder: cachedAuthor,
|
|
150
|
+
})
|
|
131
151
|
|
|
132
152
|
if (p.isCancel(author)) {
|
|
133
|
-
p.cancel('操作已取消')
|
|
134
|
-
process.exit(0)
|
|
153
|
+
p.cancel('操作已取消')
|
|
154
|
+
process.exit(0)
|
|
135
155
|
}
|
|
136
156
|
|
|
137
157
|
if (!author || author === '') {
|
|
138
|
-
author = cachedAuthor
|
|
158
|
+
author = cachedAuthor
|
|
139
159
|
} else {
|
|
140
160
|
// 保存新的作者名字
|
|
141
|
-
saveAuthorName(author)
|
|
161
|
+
saveAuthorName(author)
|
|
142
162
|
}
|
|
143
163
|
|
|
144
164
|
// Step 6: 检查目录是否存在(交互确认删除)
|
|
145
165
|
if (existsSync(projectPath)) {
|
|
146
|
-
console.log('')
|
|
147
|
-
console.log(chalk.yellow(`⚠️ 目录已存在: ${projectPath}`))
|
|
166
|
+
console.log('')
|
|
167
|
+
console.log(chalk.yellow(`⚠️ 目录已存在: ${projectPath}`))
|
|
148
168
|
|
|
149
169
|
const shouldDelete = await p.confirm({
|
|
150
170
|
message: '是否删除现有目录并重新创建?',
|
|
151
|
-
initialValue: true
|
|
152
|
-
})
|
|
171
|
+
initialValue: true,
|
|
172
|
+
})
|
|
153
173
|
|
|
154
174
|
if (p.isCancel(shouldDelete)) {
|
|
155
|
-
p.cancel('操作已取消')
|
|
156
|
-
process.exit(0)
|
|
175
|
+
p.cancel('操作已取消')
|
|
176
|
+
process.exit(0)
|
|
157
177
|
}
|
|
158
178
|
|
|
159
179
|
if (shouldDelete) {
|
|
160
|
-
const { rmSync } = await import('fs')
|
|
180
|
+
const { rmSync } = await import('fs')
|
|
161
181
|
try {
|
|
162
|
-
rmSync(projectPath, { recursive: true, force: true })
|
|
163
|
-
console.log(chalk.green(`✓ 已删除目录: ${projectPath}`))
|
|
182
|
+
rmSync(projectPath, { recursive: true, force: true })
|
|
183
|
+
console.log(chalk.green(`✓ 已删除目录: ${projectPath}`))
|
|
164
184
|
} catch (error) {
|
|
165
|
-
p.cancel(`删除目录失败: ${error.message}`)
|
|
166
|
-
process.exit(1)
|
|
185
|
+
p.cancel(`删除目录失败: ${error.message}`)
|
|
186
|
+
process.exit(1)
|
|
167
187
|
}
|
|
168
188
|
} else {
|
|
169
|
-
p.cancel('操作已取消')
|
|
170
|
-
process.exit(0)
|
|
189
|
+
p.cancel('操作已取消')
|
|
190
|
+
process.exit(0)
|
|
171
191
|
}
|
|
172
192
|
}
|
|
173
193
|
|
|
174
194
|
// Step 7: 显示信息并确认
|
|
175
|
-
const templateDisplay =
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
console.log(
|
|
181
|
-
console.log('')
|
|
182
|
-
console.log(
|
|
183
|
-
console.log(` ${chalk.gray('
|
|
184
|
-
console.log(` ${chalk.gray('
|
|
185
|
-
console.log(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
console.log('')
|
|
195
|
+
const templateDisplay =
|
|
196
|
+
templateSelection?.kind === 'builtin'
|
|
197
|
+
? getTemplate(templateSelection.name).displayName
|
|
198
|
+
: ConfigManager.getInstance().getTemplate(templateSelection.id)?.name || '自定义模板'
|
|
199
|
+
|
|
200
|
+
console.log('')
|
|
201
|
+
console.log(chalk.cyan('📋 项目信息确认:'))
|
|
202
|
+
console.log('')
|
|
203
|
+
console.log(` ${chalk.gray('项目名称:')} ${chalk.green(finalProjectName)}`)
|
|
204
|
+
console.log(` ${chalk.gray('项目模板:')} ${chalk.green(templateDisplay)}`)
|
|
205
|
+
console.log(
|
|
206
|
+
` ${chalk.gray('网络层:')} ${chalk.green(includeNetworkLayer ? '包含' : '不包含')}`,
|
|
207
|
+
)
|
|
208
|
+
console.log(` ${chalk.gray('项目路径:')} ${chalk.green(projectPath)}`)
|
|
209
|
+
console.log(` ${chalk.gray('包名:')} ${chalk.green(packageName)}`)
|
|
210
|
+
console.log(` ${chalk.gray('作者:')} ${chalk.green(author)}`)
|
|
211
|
+
console.log('')
|
|
189
212
|
|
|
190
213
|
const shouldContinue = await p.confirm({
|
|
191
|
-
message: '确认创建项目?'
|
|
192
|
-
})
|
|
214
|
+
message: '确认创建项目?',
|
|
215
|
+
})
|
|
193
216
|
|
|
194
217
|
if (!shouldContinue || p.isCancel(shouldContinue)) {
|
|
195
|
-
p.cancel('操作已取消')
|
|
196
|
-
process.exit(0)
|
|
218
|
+
p.cancel('操作已取消')
|
|
219
|
+
process.exit(0)
|
|
197
220
|
}
|
|
198
221
|
|
|
199
222
|
// Step 8: 创建项目
|
|
200
|
-
await createProject(
|
|
201
|
-
|
|
223
|
+
await createProject(
|
|
224
|
+
finalProjectName,
|
|
225
|
+
templateSelection,
|
|
226
|
+
projectPath,
|
|
227
|
+
{ projectName: finalProjectName, packageName, author, stateManager, includeNetworkLayer },
|
|
228
|
+
options,
|
|
229
|
+
)
|
|
202
230
|
} else {
|
|
203
231
|
// ========== 命令行模式 ==========
|
|
204
232
|
if (!isValidProjectName(finalProjectName)) {
|
|
205
|
-
|
|
206
|
-
|
|
233
|
+
const msg = '项目名称只能包含小写字母、数字和下划线'
|
|
234
|
+
if (jsonMode) throw new Error(msg)
|
|
235
|
+
p.cancel(msg)
|
|
236
|
+
process.exit(1)
|
|
207
237
|
}
|
|
208
238
|
|
|
209
|
-
//
|
|
239
|
+
// 如果通过参数传入模板名称,优先识别内置模板,其次识别已保存的自定义模板 ID
|
|
210
240
|
if (!templateSelection && options.template) {
|
|
211
|
-
|
|
241
|
+
const customTemplate = ConfigManager.getInstance().getTemplate(options.template)
|
|
242
|
+
templateSelection = customTemplate
|
|
243
|
+
? { kind: 'custom', id: customTemplate.id }
|
|
244
|
+
: { kind: 'builtin', name: options.template }
|
|
212
245
|
}
|
|
213
246
|
if (!templateSelection) {
|
|
214
|
-
templateSelection = await selectTemplateWithEnquirer()
|
|
247
|
+
templateSelection = await selectTemplateWithEnquirer()
|
|
215
248
|
}
|
|
216
249
|
if (templateSelection.kind === 'builtin' && !isValidTemplate(templateSelection.name)) {
|
|
217
|
-
|
|
218
|
-
|
|
250
|
+
const msg = `模板 "${templateSelection.name}" 不存在`
|
|
251
|
+
if (jsonMode) throw new Error(msg)
|
|
252
|
+
p.cancel(msg)
|
|
253
|
+
process.exit(1)
|
|
219
254
|
}
|
|
220
255
|
|
|
221
|
-
const projectDir = join(options.dir, finalProjectName)
|
|
256
|
+
const projectDir = join(options.dir, finalProjectName)
|
|
222
257
|
|
|
223
258
|
// 检查目录是否存在(命令行模式也提供删除确认)
|
|
224
259
|
if (existsSync(projectDir)) {
|
|
225
260
|
if (nonInteractive) {
|
|
226
|
-
const { rmSync } = await import('fs')
|
|
227
|
-
rmSync(projectDir, { recursive: true, force: true })
|
|
261
|
+
const { rmSync } = await import('fs')
|
|
262
|
+
rmSync(projectDir, { recursive: true, force: true })
|
|
228
263
|
} else {
|
|
229
|
-
console.log('')
|
|
230
|
-
console.log(chalk.yellow(`⚠️ 目录已存在: ${projectDir}`))
|
|
231
|
-
const shouldDelete = await p.confirm({
|
|
264
|
+
console.log('')
|
|
265
|
+
console.log(chalk.yellow(`⚠️ 目录已存在: ${projectDir}`))
|
|
266
|
+
const shouldDelete = await p.confirm({
|
|
267
|
+
message: '是否删除现有目录并重新创建?',
|
|
268
|
+
initialValue: true,
|
|
269
|
+
})
|
|
232
270
|
if (p.isCancel(shouldDelete)) {
|
|
233
|
-
p.cancel('操作已取消')
|
|
234
|
-
process.exit(0)
|
|
271
|
+
p.cancel('操作已取消')
|
|
272
|
+
process.exit(0)
|
|
235
273
|
}
|
|
236
274
|
if (shouldDelete) {
|
|
237
|
-
const { rmSync } = await import('fs')
|
|
275
|
+
const { rmSync } = await import('fs')
|
|
238
276
|
try {
|
|
239
|
-
rmSync(projectDir, { recursive: true, force: true })
|
|
240
|
-
console.log(chalk.green(`✓ 已删除目录: ${projectDir}`))
|
|
277
|
+
rmSync(projectDir, { recursive: true, force: true })
|
|
278
|
+
console.log(chalk.green(`✓ 已删除目录: ${projectDir}`))
|
|
241
279
|
} catch (error) {
|
|
242
|
-
p.cancel(`删除目录失败: ${error.message}`)
|
|
243
|
-
process.exit(1)
|
|
280
|
+
p.cancel(`删除目录失败: ${error.message}`)
|
|
281
|
+
process.exit(1)
|
|
244
282
|
}
|
|
245
283
|
} else {
|
|
246
|
-
p.cancel('操作已取消')
|
|
247
|
-
process.exit(0)
|
|
284
|
+
p.cancel('操作已取消')
|
|
285
|
+
process.exit(0)
|
|
248
286
|
}
|
|
249
287
|
}
|
|
250
288
|
}
|
|
251
289
|
|
|
252
|
-
const defaultPackageName = `com.example.${finalProjectName}
|
|
253
|
-
let packageName = options.package || defaultPackageName
|
|
290
|
+
const defaultPackageName = `com.example.${finalProjectName}`
|
|
291
|
+
let packageName = options.package || defaultPackageName
|
|
254
292
|
if (!options.package && !nonInteractive) {
|
|
255
|
-
packageName = await p.text({
|
|
293
|
+
packageName = await p.text({
|
|
294
|
+
message: `请输入包名(直接回车使用: ${defaultPackageName})`,
|
|
295
|
+
placeholder: defaultPackageName,
|
|
296
|
+
})
|
|
256
297
|
if (p.isCancel(packageName)) {
|
|
257
|
-
p.cancel('操作已取消')
|
|
258
|
-
process.exit(0)
|
|
298
|
+
p.cancel('操作已取消')
|
|
299
|
+
process.exit(0)
|
|
259
300
|
}
|
|
260
301
|
if (!packageName || packageName === '') {
|
|
261
|
-
packageName = defaultPackageName
|
|
302
|
+
packageName = defaultPackageName
|
|
262
303
|
}
|
|
263
304
|
}
|
|
264
305
|
|
|
265
|
-
let author = options.author || 'Your Name'
|
|
306
|
+
let author = options.author || 'Your Name'
|
|
266
307
|
if (!options.author && !nonInteractive) {
|
|
267
|
-
author = await p.text({
|
|
308
|
+
author = await p.text({
|
|
309
|
+
message: '请输入作者名称(直接回车使用: Your Name)',
|
|
310
|
+
placeholder: 'Your Name',
|
|
311
|
+
})
|
|
268
312
|
if (p.isCancel(author)) {
|
|
269
|
-
p.cancel('操作已取消')
|
|
270
|
-
process.exit(0)
|
|
313
|
+
p.cancel('操作已取消')
|
|
314
|
+
process.exit(0)
|
|
271
315
|
}
|
|
272
316
|
if (!author || author === '') {
|
|
273
|
-
author = 'Your Name'
|
|
317
|
+
author = 'Your Name'
|
|
274
318
|
}
|
|
275
319
|
}
|
|
276
320
|
|
|
277
|
-
// CLI 模式支持 --state
|
|
278
|
-
const
|
|
279
|
-
|
|
321
|
+
// CLI 模式支持 --state 传参;Native 默认保持纯 Flutter,显式 --network 才注入 Core 能力。
|
|
322
|
+
const isNativeTemplate =
|
|
323
|
+
templateSelection.kind === 'builtin' && templateSelection.name === 'native'
|
|
324
|
+
const includeNetworkLayer =
|
|
325
|
+
options.network !== undefined ? options.network !== false : !isNativeTemplate
|
|
326
|
+
const vNextOptions = buildVNextProjectOptions(options)
|
|
327
|
+
return await createProject(
|
|
328
|
+
finalProjectName,
|
|
329
|
+
templateSelection,
|
|
330
|
+
projectDir,
|
|
331
|
+
{
|
|
332
|
+
projectName: finalProjectName,
|
|
333
|
+
packageName,
|
|
334
|
+
author,
|
|
335
|
+
stateManager,
|
|
336
|
+
includeNetworkLayer,
|
|
337
|
+
...vNextOptions,
|
|
338
|
+
},
|
|
339
|
+
options,
|
|
340
|
+
)
|
|
280
341
|
}
|
|
281
342
|
} catch (error) {
|
|
282
|
-
|
|
283
|
-
|
|
343
|
+
if (jsonMode) throw error
|
|
344
|
+
p.cancel(`创建失败: ${error.message}`)
|
|
345
|
+
process.exit(1)
|
|
284
346
|
}
|
|
285
347
|
}
|
|
286
348
|
|
|
287
349
|
/**
|
|
288
350
|
* 创建项目
|
|
289
351
|
*/
|
|
290
|
-
async function createProject
|
|
291
|
-
const s = p.spinner()
|
|
352
|
+
async function createProject(projectName, templateSelection, projectDir, projectInfo, options) {
|
|
353
|
+
const s = p.spinner()
|
|
354
|
+
const jsonMode = process.env.FLU_CLI_JSON_OUTPUT === '1'
|
|
292
355
|
|
|
293
356
|
try {
|
|
294
357
|
// 1. 设置生成选项
|
|
295
|
-
const templateId =
|
|
296
|
-
? templateSelection.name
|
|
297
|
-
|
|
358
|
+
const templateId =
|
|
359
|
+
templateSelection.kind === 'builtin' ? templateSelection.name : templateSelection.id
|
|
360
|
+
const shouldCreateProjectConfig =
|
|
361
|
+
templateSelection.kind === 'builtin' && ['lite', 'modular', 'clean'].includes(templateId)
|
|
298
362
|
|
|
299
363
|
const generatorOptions = {
|
|
300
364
|
templateType: templateId,
|
|
301
365
|
stateManager: projectInfo.stateManager || 'default',
|
|
302
366
|
packageName: projectInfo.packageName,
|
|
303
367
|
outputDir: dirname(projectDir),
|
|
304
|
-
createFlutterProject: true, //
|
|
368
|
+
createFlutterProject: true, // 新建项目默认需要 flutter create
|
|
305
369
|
forceUpdate: !options.cache,
|
|
306
370
|
author: projectInfo.author,
|
|
307
371
|
flutterTemplate: options.flutterTemplate || 'app',
|
|
308
|
-
includeNetworkLayer: projectInfo.includeNetworkLayer !== false // 默认包含网络层
|
|
309
|
-
|
|
372
|
+
includeNetworkLayer: projectInfo.includeNetworkLayer !== false, // 默认包含网络层
|
|
373
|
+
examples: projectInfo.examples || [],
|
|
374
|
+
composition: projectInfo.composition,
|
|
375
|
+
architectureMode: projectInfo.architectureMode,
|
|
376
|
+
enableMixinOptions: projectInfo.enableMixinOptions,
|
|
377
|
+
platforms: projectInfo.platforms,
|
|
378
|
+
flutterSdk: projectInfo.flutterSdk,
|
|
379
|
+
capabilities: projectInfo.capabilities,
|
|
380
|
+
helpers: projectInfo.helpers,
|
|
381
|
+
createProjectConfig: shouldCreateProjectConfig,
|
|
382
|
+
}
|
|
310
383
|
|
|
311
384
|
// 2. 调用核心生成器
|
|
312
|
-
s.start('正在生成项目 (Core Engine)...')
|
|
313
|
-
const pg = new ProjectGenerator()
|
|
314
|
-
const success = await pg.generate(projectName, generatorOptions)
|
|
385
|
+
s.start('正在生成项目 (Core Engine)...')
|
|
386
|
+
const pg = new ProjectGenerator()
|
|
387
|
+
const success = await pg.generate(projectName, generatorOptions)
|
|
315
388
|
|
|
316
389
|
if (!success) {
|
|
317
|
-
s.stop('项目生成失败')
|
|
318
|
-
throw new Error('核心生成器返回失败')
|
|
390
|
+
s.stop('项目生成失败')
|
|
391
|
+
throw new Error('核心生成器返回失败')
|
|
319
392
|
}
|
|
320
|
-
s.stop('✓ 项目生成成功')
|
|
393
|
+
s.stop('✓ 项目生成成功')
|
|
321
394
|
|
|
322
395
|
// 3. CLI 特色功能:同步标准代码片段 (Native 模板跳过)
|
|
323
|
-
const isNative = templateId === 'native' || templateId === 'none'
|
|
396
|
+
const isNative = templateId === 'native' || templateId === 'none'
|
|
324
397
|
if (!isNative) {
|
|
325
|
-
s.start('正在同步代码片段...')
|
|
326
|
-
await syncSnippets(projectDir)
|
|
327
|
-
s.stop('✓ 代码片段同步完成')
|
|
398
|
+
s.start('正在同步代码片段...')
|
|
399
|
+
await syncSnippets(projectDir)
|
|
400
|
+
s.stop('✓ 代码片段同步完成')
|
|
328
401
|
}
|
|
329
402
|
|
|
330
403
|
// 4. 清理模板残余(ProjectGenerator 可能已经做了一部分,这里确保万一)
|
|
331
|
-
cleanupTemplateFiles(projectDir)
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
404
|
+
cleanupTemplateFiles(projectDir)
|
|
405
|
+
|
|
406
|
+
if (!jsonMode) {
|
|
407
|
+
p.outro(
|
|
408
|
+
chalk.green.bold('✨ 项目创建成功!\n\n') +
|
|
409
|
+
chalk.cyan('下一步:\n') +
|
|
410
|
+
chalk.yellow(` cd ${projectName}\n`) +
|
|
411
|
+
chalk.yellow(` flutter pub get\n`) +
|
|
412
|
+
chalk.yellow(` flutter run`),
|
|
413
|
+
)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// 5. 提示资源配置(非交互模式下跳过,避免 CI / 脚本挂起)
|
|
417
|
+
const nonInteractiveExit = process.env.FLU_CLI_NON_INTERACTIVE === '1'
|
|
418
|
+
if (!nonInteractiveExit) {
|
|
419
|
+
const shouldConfigAssets = await p.confirm({
|
|
420
|
+
message: '项目已创建。是否现在就配置 App 图标和启动图?',
|
|
421
|
+
initialValue: false,
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
if (shouldConfigAssets) {
|
|
425
|
+
await configAssets({ dir: projectDir })
|
|
426
|
+
}
|
|
350
427
|
}
|
|
351
428
|
|
|
352
429
|
// 6. 记忆默认模板
|
|
353
430
|
if (templateSelection?.kind === 'builtin') {
|
|
354
|
-
saveDefaultTemplate({ type: 'builtin', idOrName: templateSelection.name })
|
|
431
|
+
saveDefaultTemplate({ type: 'builtin', idOrName: templateSelection.name })
|
|
355
432
|
} else if (templateSelection?.kind === 'custom') {
|
|
356
|
-
saveDefaultTemplate({ type: 'custom', idOrName: templateSelection.id })
|
|
433
|
+
saveDefaultTemplate({ type: 'custom', idOrName: templateSelection.id })
|
|
357
434
|
}
|
|
358
435
|
|
|
359
|
-
|
|
360
|
-
|
|
436
|
+
if (!jsonMode) process.exit(0)
|
|
437
|
+
return {
|
|
438
|
+
ok: true,
|
|
439
|
+
projectName,
|
|
440
|
+
projectDir,
|
|
441
|
+
templateId,
|
|
442
|
+
stateManager: generatorOptions.stateManager,
|
|
443
|
+
packageName: generatorOptions.packageName,
|
|
444
|
+
includeNetworkLayer: generatorOptions.includeNetworkLayer,
|
|
445
|
+
examples: generatorOptions.examples,
|
|
446
|
+
platforms: generatorOptions.platforms,
|
|
447
|
+
flutterSdk: generatorOptions.flutterSdk,
|
|
448
|
+
capabilities: generatorOptions.capabilities,
|
|
449
|
+
helpers: generatorOptions.helpers,
|
|
450
|
+
snippetsSynced: !isNative,
|
|
451
|
+
}
|
|
361
452
|
} catch (error) {
|
|
362
|
-
s.stop('创建失败')
|
|
363
|
-
throw error
|
|
453
|
+
s.stop('创建失败')
|
|
454
|
+
throw error
|
|
364
455
|
}
|
|
365
456
|
}
|
|
366
457
|
|
|
367
458
|
/**
|
|
368
459
|
* 验证项目名称
|
|
369
460
|
*/
|
|
370
|
-
function isValidProjectName
|
|
371
|
-
return /^[a-z][a-z0-9_]*$/.test(name)
|
|
461
|
+
function isValidProjectName(name) {
|
|
462
|
+
return /^[a-z][a-z0-9_]*$/.test(name)
|
|
372
463
|
}
|