flu-cli-core 1.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.
- package/README.md +60 -0
- package/dist/chunk-FOMWV2YP.js +378 -0
- package/dist/chunk-SW6YDKXI.js +112 -0
- package/dist/factory-6DDXZYQP.js +6 -0
- package/dist/index.cjs +4668 -0
- package/dist/index.d.cts +644 -0
- package/dist/index.d.ts +644 -0
- package/dist/index.js +4037 -0
- package/dist/upgrade_snippets-XFR7Q444.js +8 -0
- package/locales/en-US.json +59 -0
- package/locales/zh-CN.json +59 -0
- package/package.json +52 -0
- package/templates/README.md +129 -0
- package/templates/core_files/base/base_list_page.dart.template +225 -0
- package/templates/core_files/base/base_list_viewmodel.dart.template +164 -0
- package/templates/core_files/base/base_page.dart.template +252 -0
- package/templates/core_files/base/base_viewmodel.dart.template +68 -0
- package/templates/core_files/base/index.dart.template +5 -0
- package/templates/core_files/config/app_config.dart.template +142 -0
- package/templates/core_files/config/app_initializer.dart.template +74 -0
- package/templates/core_files/config/index.dart.template +3 -0
- package/templates/core_files/index.dart.template +8 -0
- package/templates/core_files/network/README.md +378 -0
- package/templates/core_files/network/app_error_code.dart.template +49 -0
- package/templates/core_files/network/app_http.dart.template +306 -0
- package/templates/core_files/network/app_response.dart.template +81 -0
- package/templates/core_files/network/index.dart.template +12 -0
- package/templates/core_files/network/interceptors/app_response_interceptor.dart.template +44 -0
- package/templates/core_files/network/interceptors/auth_interceptor.dart.template +30 -0
- package/templates/core_files/network/interceptors/error_interceptor.dart.template +48 -0
- package/templates/core_files/network/interceptors/index.dart.template +6 -0
- package/templates/core_files/network/interceptors/log_interceptor.dart.template +97 -0
- package/templates/core_files/network/interceptors/network_error_interceptor.dart.template +58 -0
- package/templates/core_files/network/interceptors/retry_interceptor.dart.template +69 -0
- package/templates/core_files/network/response_adapter.dart.template +69 -0
- package/templates/core_files/router/app_routes.dart.template +32 -0
- package/templates/core_files/router/index.dart.template +3 -0
- package/templates/core_files/router/navigator_util_getx.dart.template +131 -0
- package/templates/core_files/router/navigator_util_material.dart.template +191 -0
- package/templates/core_files/storage/index.dart.template +3 -0
- package/templates/core_files/storage/storage_keys.dart.template +34 -0
- package/templates/core_files/storage/storage_util.dart.template +102 -0
- package/templates/core_files/theme/app_theme.dart.template +37 -0
- package/templates/core_files/theme/index.dart.template +3 -0
- package/templates/core_files/theme/status_views_theme.dart.template +40 -0
- package/templates/core_files/utils/index.dart.template +2 -0
- package/templates/core_files/utils/loading_util.dart.template +55 -0
- package/templates/core_files/utils/toast_util.dart.template +128 -0
- package/templates/examples/eg_list_page.dart.template +340 -0
- package/templates/examples/eg_list_viewmodel.dart.template +31 -0
- package/templates/examples/eg_service.dart.template +78 -0
- package/templates/examples/mock_data.dart.template +50388 -0
- package/templates/examples/tu_chong_model.dart.template +633 -0
- package/templates/request_helper.dart.template +59 -0
- package/templates/snippets/flu-cli.code-snippets +268 -0
- package/templates/snippets/flu-cli.code-snippets.backup +268 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4037 @@
|
|
|
1
|
+
import {
|
|
2
|
+
StateManagerAdapterFactory
|
|
3
|
+
} from "./chunk-FOMWV2YP.js";
|
|
4
|
+
import {
|
|
5
|
+
ConsoleLogger,
|
|
6
|
+
checkSnippetsVersion,
|
|
7
|
+
logger,
|
|
8
|
+
upgradeSnippets
|
|
9
|
+
} from "./chunk-SW6YDKXI.js";
|
|
10
|
+
|
|
11
|
+
// src/utils/string_helper.ts
|
|
12
|
+
function toPascalCase(str) {
|
|
13
|
+
return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
14
|
+
}
|
|
15
|
+
function toSnakeCase(str) {
|
|
16
|
+
return str.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
|
|
17
|
+
}
|
|
18
|
+
function toCamelCase(str) {
|
|
19
|
+
const pascal = toPascalCase(str);
|
|
20
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
21
|
+
}
|
|
22
|
+
function toTitleCase(str) {
|
|
23
|
+
return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ");
|
|
24
|
+
}
|
|
25
|
+
function toKebabCase(str) {
|
|
26
|
+
return str.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/utils/project_detector.ts
|
|
30
|
+
import { existsSync as existsSync2 } from "fs";
|
|
31
|
+
import { join as join2 } from "path";
|
|
32
|
+
import fsx from "fs-extra";
|
|
33
|
+
|
|
34
|
+
// src/utils/project_config.ts
|
|
35
|
+
import { existsSync, readFileSync } from "fs";
|
|
36
|
+
import { join } from "path";
|
|
37
|
+
var ProjectConfigManager = class {
|
|
38
|
+
static CONFIG_FILE_NAME = ".flu-cli.json";
|
|
39
|
+
/**
|
|
40
|
+
* 加载项目配置
|
|
41
|
+
* @param projectDir 项目目录
|
|
42
|
+
* @returns 配置对象,如果不存在则返回 null
|
|
43
|
+
*/
|
|
44
|
+
static loadConfig(projectDir) {
|
|
45
|
+
try {
|
|
46
|
+
const configPath = join(projectDir, this.CONFIG_FILE_NAME);
|
|
47
|
+
if (!existsSync(configPath)) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const content = readFileSync(configPath, "utf-8");
|
|
51
|
+
const config = JSON.parse(content);
|
|
52
|
+
return config;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 检查项目是否有配置文件
|
|
59
|
+
* @param projectDir 项目目录
|
|
60
|
+
* @returns 是否存在配置文件
|
|
61
|
+
*/
|
|
62
|
+
static hasConfig(projectDir) {
|
|
63
|
+
const configPath = join(projectDir, this.CONFIG_FILE_NAME);
|
|
64
|
+
return existsSync(configPath);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 获取指定生成器的配置
|
|
68
|
+
* @param projectDir 项目目录
|
|
69
|
+
* @param generatorType 生成器类型
|
|
70
|
+
* @returns 生成器配置,如果不存在则返回 null
|
|
71
|
+
*/
|
|
72
|
+
static getGeneratorConfig(projectDir, generatorType) {
|
|
73
|
+
const config = this.loadConfig(projectDir);
|
|
74
|
+
return config?.generators?.[generatorType] || null;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 获取默认配置模板
|
|
78
|
+
* @param templateType 模板类型
|
|
79
|
+
* @returns 配置模板对象
|
|
80
|
+
*/
|
|
81
|
+
static getDefaultConfigTemplate(templateType) {
|
|
82
|
+
const t2 = String(templateType).toLowerCase();
|
|
83
|
+
if (t2.includes("lite")) {
|
|
84
|
+
return {
|
|
85
|
+
template: "lite",
|
|
86
|
+
generators: {
|
|
87
|
+
page: {
|
|
88
|
+
path: "lib/pages",
|
|
89
|
+
fileSuffix: "_page",
|
|
90
|
+
defaultType: "stateful",
|
|
91
|
+
withBasePage: true,
|
|
92
|
+
basePageClass: "BasePage",
|
|
93
|
+
basePageImport: "lib/core/index.dart",
|
|
94
|
+
withViewModel: true,
|
|
95
|
+
viewModelPath: "lib/viewmodels"
|
|
96
|
+
},
|
|
97
|
+
viewModel: {
|
|
98
|
+
path: "lib/viewmodels",
|
|
99
|
+
fileSuffix: "_viewmodel",
|
|
100
|
+
withBaseViewModel: true,
|
|
101
|
+
baseViewModelClass: "BaseViewModel",
|
|
102
|
+
baseViewModelImport: "lib/core/index.dart"
|
|
103
|
+
},
|
|
104
|
+
widget: {
|
|
105
|
+
path: "lib/widgets",
|
|
106
|
+
fileSuffix: "_widget",
|
|
107
|
+
defaultType: "stateless"
|
|
108
|
+
},
|
|
109
|
+
model: {
|
|
110
|
+
path: "lib/models",
|
|
111
|
+
fileSuffix: "_model"
|
|
112
|
+
},
|
|
113
|
+
component: {
|
|
114
|
+
path: "lib/components"
|
|
115
|
+
},
|
|
116
|
+
service: {
|
|
117
|
+
path: "lib/services"
|
|
118
|
+
},
|
|
119
|
+
module: {
|
|
120
|
+
path: "lib/features/{feature}"
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
} else if (t2.includes("modular")) {
|
|
125
|
+
return {
|
|
126
|
+
template: "modular",
|
|
127
|
+
generators: {
|
|
128
|
+
page: {
|
|
129
|
+
path: "lib/features/{feature}/pages",
|
|
130
|
+
fileSuffix: "_page",
|
|
131
|
+
defaultType: "stateful",
|
|
132
|
+
withBasePage: true,
|
|
133
|
+
basePageClass: "BasePage",
|
|
134
|
+
basePageImport: "lib/core/index.dart",
|
|
135
|
+
withViewModel: true,
|
|
136
|
+
viewModelPath: "lib/features/{feature}/viewmodels"
|
|
137
|
+
},
|
|
138
|
+
viewModel: {
|
|
139
|
+
path: "lib/features/{feature}/viewmodels",
|
|
140
|
+
fileSuffix: "_viewmodel",
|
|
141
|
+
withBaseViewModel: true,
|
|
142
|
+
baseViewModelClass: "BaseViewModel",
|
|
143
|
+
baseViewModelImport: "lib/core/index.dart"
|
|
144
|
+
},
|
|
145
|
+
widget: {
|
|
146
|
+
path: "lib/shared/widgets",
|
|
147
|
+
fileSuffix: "_widget",
|
|
148
|
+
defaultType: "stateless"
|
|
149
|
+
},
|
|
150
|
+
model: {
|
|
151
|
+
path: "lib/shared/models",
|
|
152
|
+
fileSuffix: "_model"
|
|
153
|
+
},
|
|
154
|
+
component: {
|
|
155
|
+
path: "lib/features/{feature}/components"
|
|
156
|
+
},
|
|
157
|
+
service: {
|
|
158
|
+
path: "lib/features/{feature}/services"
|
|
159
|
+
},
|
|
160
|
+
module: {
|
|
161
|
+
path: "lib/features/{feature}"
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
} else if (t2.includes("clean")) {
|
|
166
|
+
return {
|
|
167
|
+
template: "clean",
|
|
168
|
+
generators: {
|
|
169
|
+
page: {
|
|
170
|
+
path: "lib/features/{feature}/presentation/pages",
|
|
171
|
+
fileSuffix: "_page",
|
|
172
|
+
defaultType: "stateful",
|
|
173
|
+
withBasePage: true,
|
|
174
|
+
basePageClass: "BasePage",
|
|
175
|
+
basePageImport: "lib/core/index.dart",
|
|
176
|
+
withViewModel: true,
|
|
177
|
+
viewModelPath: "lib/features/{feature}/presentation/viewmodels"
|
|
178
|
+
},
|
|
179
|
+
viewModel: {
|
|
180
|
+
path: "lib/features/{feature}/presentation/viewmodels",
|
|
181
|
+
fileSuffix: "_viewmodel",
|
|
182
|
+
withBaseViewModel: true,
|
|
183
|
+
baseViewModelClass: "BaseViewModel",
|
|
184
|
+
baseViewModelImport: "lib/core/index.dart"
|
|
185
|
+
},
|
|
186
|
+
widget: {
|
|
187
|
+
path: "lib/shared/widgets",
|
|
188
|
+
fileSuffix: "_widget",
|
|
189
|
+
defaultType: "stateless"
|
|
190
|
+
},
|
|
191
|
+
model: {
|
|
192
|
+
path: "lib/features/{feature}/data/models",
|
|
193
|
+
fileSuffix: "_model"
|
|
194
|
+
},
|
|
195
|
+
component: {
|
|
196
|
+
path: "lib/features/{feature}/presentation/components"
|
|
197
|
+
},
|
|
198
|
+
service: {
|
|
199
|
+
path: "lib/features/{feature}/data/datasources"
|
|
200
|
+
},
|
|
201
|
+
module: {
|
|
202
|
+
path: "lib/features/{feature}"
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
} else {
|
|
207
|
+
return {
|
|
208
|
+
generators: {
|
|
209
|
+
page: {
|
|
210
|
+
path: "lib/pages",
|
|
211
|
+
defaultType: "stateful",
|
|
212
|
+
withBasePage: false,
|
|
213
|
+
withViewModel: false
|
|
214
|
+
},
|
|
215
|
+
viewModel: {
|
|
216
|
+
path: "lib/viewmodels",
|
|
217
|
+
withBaseViewModel: false
|
|
218
|
+
},
|
|
219
|
+
widget: {
|
|
220
|
+
path: "lib/widgets",
|
|
221
|
+
defaultType: "stateless"
|
|
222
|
+
},
|
|
223
|
+
model: {
|
|
224
|
+
path: "lib/models"
|
|
225
|
+
},
|
|
226
|
+
component: {
|
|
227
|
+
path: "lib/components"
|
|
228
|
+
},
|
|
229
|
+
service: {
|
|
230
|
+
path: "lib/services"
|
|
231
|
+
},
|
|
232
|
+
module: {
|
|
233
|
+
path: "lib/features/{feature}"
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// src/utils/project_detector.ts
|
|
242
|
+
function detectProjectTemplate(projectDir = process.cwd()) {
|
|
243
|
+
try {
|
|
244
|
+
const config = ProjectConfigManager.loadConfig(projectDir);
|
|
245
|
+
if (config?.template) {
|
|
246
|
+
const t2 = String(config.template).toLowerCase();
|
|
247
|
+
if (t2.includes("modular")) return "modular";
|
|
248
|
+
if (t2.includes("clean")) return "clean";
|
|
249
|
+
if (t2.includes("lite")) return "lite";
|
|
250
|
+
return t2;
|
|
251
|
+
}
|
|
252
|
+
const libDir = join2(projectDir, "lib");
|
|
253
|
+
if (!existsSync2(libDir)) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
const featuresDir = join2(libDir, "features");
|
|
257
|
+
if (existsSync2(featuresDir)) {
|
|
258
|
+
const dirs = fsx.readdirSync(featuresDir);
|
|
259
|
+
const anyFeatureDir = dirs.find((d) => !d.startsWith("."));
|
|
260
|
+
if (anyFeatureDir) {
|
|
261
|
+
const presentationDir = join2(featuresDir, anyFeatureDir, "presentation");
|
|
262
|
+
if (existsSync2(presentationDir)) {
|
|
263
|
+
return "clean";
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const coreDir = join2(libDir, "core");
|
|
267
|
+
if (existsSync2(coreDir)) {
|
|
268
|
+
if (existsSync2(join2(coreDir, "usecases")) || existsSync2(join2(coreDir, "domain"))) {
|
|
269
|
+
return "clean";
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return "modular";
|
|
273
|
+
}
|
|
274
|
+
const pagesDir = join2(libDir, "pages");
|
|
275
|
+
const viewmodelsDir = join2(libDir, "viewmodels");
|
|
276
|
+
const widgetsDir = join2(libDir, "widgets");
|
|
277
|
+
if (existsSync2(pagesDir) && existsSync2(viewmodelsDir) && existsSync2(widgetsDir)) {
|
|
278
|
+
return "lite";
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
281
|
+
} catch (error) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function isCustomTemplateProject(projectDir = process.cwd()) {
|
|
286
|
+
const template = detectProjectTemplate(projectDir);
|
|
287
|
+
return template === null;
|
|
288
|
+
}
|
|
289
|
+
function getStateManager(projectDir = process.cwd()) {
|
|
290
|
+
try {
|
|
291
|
+
const pubspec = join2(projectDir, "pubspec.yaml");
|
|
292
|
+
if (existsSync2(pubspec)) {
|
|
293
|
+
const content = fsx.readFileSync(pubspec, "utf8");
|
|
294
|
+
if (/flutter_riverpod\s*:/m.test(content)) return "riverpod";
|
|
295
|
+
if (/\n\s*get\s*:/m.test(content)) return "getx";
|
|
296
|
+
if (/\n\s*provider\s*:/m.test(content)) return "provider";
|
|
297
|
+
}
|
|
298
|
+
} catch {
|
|
299
|
+
}
|
|
300
|
+
return "default";
|
|
301
|
+
}
|
|
302
|
+
function getPagePath(projectDir, moduleName) {
|
|
303
|
+
const config = ProjectConfigManager.getGeneratorConfig(projectDir, "page");
|
|
304
|
+
if (config?.path) {
|
|
305
|
+
return join2(projectDir, config.path.replace("{feature}", moduleName));
|
|
306
|
+
}
|
|
307
|
+
const template = detectProjectTemplate(projectDir);
|
|
308
|
+
switch (template) {
|
|
309
|
+
case "lite":
|
|
310
|
+
return join2(projectDir, "lib", "pages");
|
|
311
|
+
case "modular":
|
|
312
|
+
return join2(projectDir, "lib", "features", moduleName, "pages");
|
|
313
|
+
case "clean":
|
|
314
|
+
return join2(projectDir, "lib", "features", moduleName, "presentation", "pages");
|
|
315
|
+
default:
|
|
316
|
+
return join2(projectDir, "lib", "pages");
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function getViewModelPath(projectDir, moduleName) {
|
|
320
|
+
const config = ProjectConfigManager.getGeneratorConfig(projectDir, "viewModel");
|
|
321
|
+
if (config?.path) {
|
|
322
|
+
return join2(projectDir, config.path.replace("{feature}", moduleName));
|
|
323
|
+
}
|
|
324
|
+
const template = detectProjectTemplate(projectDir);
|
|
325
|
+
switch (template) {
|
|
326
|
+
case "lite":
|
|
327
|
+
return join2(projectDir, "lib", "viewmodels");
|
|
328
|
+
case "modular":
|
|
329
|
+
return join2(projectDir, "lib", "features", moduleName, "viewmodels");
|
|
330
|
+
case "clean":
|
|
331
|
+
return join2(projectDir, "lib", "features", moduleName, "presentation", "viewmodels");
|
|
332
|
+
default:
|
|
333
|
+
return join2(projectDir, "lib", "viewmodels");
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function getWidgetPath(projectDir, moduleName = null) {
|
|
337
|
+
const config = ProjectConfigManager.getGeneratorConfig(projectDir, "widget");
|
|
338
|
+
if (config?.path) {
|
|
339
|
+
return join2(projectDir, config.path.replace("{feature}", moduleName || ""));
|
|
340
|
+
}
|
|
341
|
+
const template = detectProjectTemplate(projectDir);
|
|
342
|
+
switch (template) {
|
|
343
|
+
case "lite":
|
|
344
|
+
return join2(projectDir, "lib", "widgets");
|
|
345
|
+
case "modular":
|
|
346
|
+
return moduleName ? join2(projectDir, "lib", "features", moduleName, "widgets") : join2(projectDir, "lib", "shared", "widgets");
|
|
347
|
+
case "clean":
|
|
348
|
+
return moduleName ? join2(projectDir, "lib", "features", moduleName, "presentation", "widgets") : join2(projectDir, "lib", "shared", "widgets");
|
|
349
|
+
default:
|
|
350
|
+
return join2(projectDir, "lib", "widgets");
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function getServicePath(projectDir, moduleName = null) {
|
|
354
|
+
const config = ProjectConfigManager.getGeneratorConfig(projectDir, "component");
|
|
355
|
+
if (config?.path) {
|
|
356
|
+
return join2(projectDir, config.path.replace("{feature}", moduleName || ""));
|
|
357
|
+
}
|
|
358
|
+
const template = detectProjectTemplate(projectDir);
|
|
359
|
+
switch (template) {
|
|
360
|
+
case "lite":
|
|
361
|
+
return join2(projectDir, "lib", "services");
|
|
362
|
+
case "modular":
|
|
363
|
+
return moduleName ? join2(projectDir, "lib", "features", moduleName, "services") : join2(projectDir, "lib", "shared", "services");
|
|
364
|
+
case "clean":
|
|
365
|
+
return moduleName ? join2(projectDir, "lib", "features", moduleName, "data", "datasources") : join2(projectDir, "lib", "core", "network");
|
|
366
|
+
default:
|
|
367
|
+
return join2(projectDir, "lib", "services");
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function getModelPath(projectDir, moduleName = null) {
|
|
371
|
+
const config = ProjectConfigManager.getGeneratorConfig(projectDir, "model");
|
|
372
|
+
if (config?.path) {
|
|
373
|
+
return join2(projectDir, config.path.replace("{feature}", moduleName || ""));
|
|
374
|
+
}
|
|
375
|
+
const template = detectProjectTemplate(projectDir);
|
|
376
|
+
switch (template) {
|
|
377
|
+
case "lite":
|
|
378
|
+
return join2(projectDir, "lib", "models");
|
|
379
|
+
case "modular":
|
|
380
|
+
return moduleName ? join2(projectDir, "lib", "features", moduleName, "models") : join2(projectDir, "lib", "shared", "models");
|
|
381
|
+
case "clean":
|
|
382
|
+
return moduleName ? join2(projectDir, "lib", "features", moduleName, "data", "models") : join2(projectDir, "lib", "shared", "models");
|
|
383
|
+
default:
|
|
384
|
+
return join2(projectDir, "lib", "models");
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
function getRelativeImportPath(fromPath, toPath) {
|
|
388
|
+
const fromParts = fromPath.split("/");
|
|
389
|
+
const toParts = toPath.split("/");
|
|
390
|
+
let commonIndex = 0;
|
|
391
|
+
while (commonIndex < fromParts.length && commonIndex < toParts.length) {
|
|
392
|
+
if (fromParts[commonIndex] !== toParts[commonIndex]) {
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
commonIndex++;
|
|
396
|
+
}
|
|
397
|
+
const upLevels = fromParts.length - commonIndex - 1;
|
|
398
|
+
const upPath = "../".repeat(upLevels);
|
|
399
|
+
const targetPath = toParts.slice(commonIndex).join("/");
|
|
400
|
+
return upPath + targetPath;
|
|
401
|
+
}
|
|
402
|
+
function getPackageName(projectDir = process.cwd()) {
|
|
403
|
+
try {
|
|
404
|
+
const pubspec = join2(projectDir, "pubspec.yaml");
|
|
405
|
+
if (!existsSync2(pubspec)) {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
const content = fsx.readFileSync(pubspec, "utf8");
|
|
409
|
+
const match = content.match(/^name:\s+(.+)$/m);
|
|
410
|
+
if (match && match[1]) {
|
|
411
|
+
return match[1].trim();
|
|
412
|
+
}
|
|
413
|
+
return null;
|
|
414
|
+
} catch (error) {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function getPackageImportPath(projectDir, targetPath) {
|
|
419
|
+
const packageName = getPackageName(projectDir);
|
|
420
|
+
if (!packageName) {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
const cleanPath = targetPath.replace(/^lib\//, "");
|
|
424
|
+
return `package:${packageName}/${cleanPath}`;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// src/utils/snippet_loader.ts
|
|
428
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
429
|
+
import { join as join3 } from "path";
|
|
430
|
+
import JSON5 from "json5";
|
|
431
|
+
var SNIPPET_FILE_NAME = "flu-cli.code-snippets";
|
|
432
|
+
function loadProjectSnippets(projectDir) {
|
|
433
|
+
try {
|
|
434
|
+
const snippetsPath = join3(projectDir, ".vscode", SNIPPET_FILE_NAME);
|
|
435
|
+
if (!existsSync3(snippetsPath)) return {};
|
|
436
|
+
const raw = readFileSync2(snippetsPath, "utf8");
|
|
437
|
+
const json = JSON5.parse(raw);
|
|
438
|
+
return json || {};
|
|
439
|
+
} catch (_) {
|
|
440
|
+
return {};
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function renderSnippet(bodyLines, variables) {
|
|
444
|
+
const body = Array.isArray(bodyLines) ? bodyLines.join("\n") : String(bodyLines || "");
|
|
445
|
+
let result = body.replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
|
446
|
+
return Object.prototype.hasOwnProperty.call(variables, key) ? String(variables[key]) : `{{${key}}}`;
|
|
447
|
+
});
|
|
448
|
+
return replaceVSCodeVariables(result, variables);
|
|
449
|
+
}
|
|
450
|
+
function replaceVSCodeVariables(text, variables) {
|
|
451
|
+
let result = "";
|
|
452
|
+
let i = 0;
|
|
453
|
+
while (i < text.length) {
|
|
454
|
+
if (text.substring(i, i + 2) === "${") {
|
|
455
|
+
let depth = 1;
|
|
456
|
+
let j = i + 2;
|
|
457
|
+
const contentStart = j;
|
|
458
|
+
while (j < text.length && depth > 0) {
|
|
459
|
+
if (text.substring(j, j + 2) === "${") {
|
|
460
|
+
depth++;
|
|
461
|
+
j += 2;
|
|
462
|
+
} else if (text[j] === "}") {
|
|
463
|
+
depth--;
|
|
464
|
+
j++;
|
|
465
|
+
} else if (text[j] === "\\") {
|
|
466
|
+
j += 2;
|
|
467
|
+
} else {
|
|
468
|
+
j++;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (depth === 0) {
|
|
472
|
+
const innerContent = text.substring(contentStart, j - 1);
|
|
473
|
+
let replacement = text.substring(i, j);
|
|
474
|
+
let separatorIndex = -1;
|
|
475
|
+
let separatorChar = "";
|
|
476
|
+
let k = 0;
|
|
477
|
+
let innerDepth = 0;
|
|
478
|
+
while (k < innerContent.length) {
|
|
479
|
+
if (innerContent.substring(k, k + 2) === "${") {
|
|
480
|
+
innerDepth++;
|
|
481
|
+
k += 2;
|
|
482
|
+
} else if (innerContent[k] === "}") {
|
|
483
|
+
innerDepth--;
|
|
484
|
+
k++;
|
|
485
|
+
} else if (innerDepth === 0 && (innerContent[k] === ":" || innerContent[k] === "/")) {
|
|
486
|
+
separatorIndex = k;
|
|
487
|
+
separatorChar = innerContent[k];
|
|
488
|
+
break;
|
|
489
|
+
} else {
|
|
490
|
+
k++;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (separatorChar === ":") {
|
|
494
|
+
const defaultValue = innerContent.substring(separatorIndex + 1);
|
|
495
|
+
const processedDefault = replaceVSCodeVariables(defaultValue, variables);
|
|
496
|
+
if (Object.prototype.hasOwnProperty.call(variables, processedDefault)) {
|
|
497
|
+
replacement = String(variables[processedDefault]);
|
|
498
|
+
} else {
|
|
499
|
+
replacement = processedDefault;
|
|
500
|
+
}
|
|
501
|
+
} else if (separatorChar === "/") {
|
|
502
|
+
if (variables.snake_name) {
|
|
503
|
+
replacement = variables.snake_name;
|
|
504
|
+
}
|
|
505
|
+
} else {
|
|
506
|
+
const key = innerContent;
|
|
507
|
+
if (Object.prototype.hasOwnProperty.call(variables, key)) {
|
|
508
|
+
replacement = String(variables[key]);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
result += replacement;
|
|
512
|
+
i = j;
|
|
513
|
+
} else {
|
|
514
|
+
result += text[i];
|
|
515
|
+
i++;
|
|
516
|
+
}
|
|
517
|
+
} else {
|
|
518
|
+
result += text[i];
|
|
519
|
+
i++;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return result;
|
|
523
|
+
}
|
|
524
|
+
function getSnippetContent(projectDir, key, variables) {
|
|
525
|
+
try {
|
|
526
|
+
const map = loadProjectSnippets(projectDir);
|
|
527
|
+
const item = map[key];
|
|
528
|
+
if (!item || !item.body) return null;
|
|
529
|
+
return renderSnippet(item.body, variables);
|
|
530
|
+
} catch (error) {
|
|
531
|
+
console.error(`Failed to parse snippet for key "${key}":`, error);
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// src/templates/template_generator.ts
|
|
537
|
+
var TemplateGenerator = class {
|
|
538
|
+
/**
|
|
539
|
+
* 生成 Stateful Page (BasePage)
|
|
540
|
+
*/
|
|
541
|
+
/**
|
|
542
|
+
* 生成 Stateful Page (BasePage)
|
|
543
|
+
*/
|
|
544
|
+
static generateStatefulPageWithBase(namePascal, nameTitle, options = {}) {
|
|
545
|
+
const {
|
|
546
|
+
vmImportPath = `../viewmodels/${namePascal.toLowerCase()}_viewmodel.dart`,
|
|
547
|
+
coreImportPath,
|
|
548
|
+
baseClass = "BasePage",
|
|
549
|
+
extraImports = []
|
|
550
|
+
} = options;
|
|
551
|
+
let imports = `import 'package:flutter/material.dart';
|
|
552
|
+
`;
|
|
553
|
+
if (extraImports.length > 0) {
|
|
554
|
+
imports += extraImports.join("\n") + "\n";
|
|
555
|
+
}
|
|
556
|
+
if (coreImportPath && !extraImports.some((i) => i.includes(coreImportPath))) {
|
|
557
|
+
imports += `import '${coreImportPath}';
|
|
558
|
+
`;
|
|
559
|
+
}
|
|
560
|
+
if (vmImportPath) imports += `import '${vmImportPath}';
|
|
561
|
+
`;
|
|
562
|
+
return `${imports}
|
|
563
|
+
class ${namePascal}Page extends ${baseClass}<${namePascal}ViewModel> {
|
|
564
|
+
const ${namePascal}Page({super.key});
|
|
565
|
+
|
|
566
|
+
@override
|
|
567
|
+
State<${namePascal}Page> createState() => _${namePascal}PageState();
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
class _${namePascal}PageState extends ${baseClass}State<${namePascal}ViewModel, ${namePascal}Page> {
|
|
571
|
+
// ==================== UI \u914D\u7F6E ====================
|
|
572
|
+
@override
|
|
573
|
+
String get title => '${nameTitle}';
|
|
574
|
+
|
|
575
|
+
// ==================== ViewModel ====================
|
|
576
|
+
@override
|
|
577
|
+
${namePascal}ViewModel createViewModel() => ${namePascal}ViewModel();
|
|
578
|
+
|
|
579
|
+
// ==================== UI \u6784\u5EFA ====================
|
|
580
|
+
@override
|
|
581
|
+
Widget buildContent(BuildContext context) {
|
|
582
|
+
return const Center(
|
|
583
|
+
child: Text('${namePascal}Page'),
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
`;
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* 生成 Simple Stateful Page
|
|
591
|
+
*/
|
|
592
|
+
static generateSimpleStatefulPage(namePascal, nameTitle) {
|
|
593
|
+
return `import 'package:flutter/material.dart';
|
|
594
|
+
|
|
595
|
+
class ${namePascal}Page extends StatefulWidget {
|
|
596
|
+
const ${namePascal}Page({super.key});
|
|
597
|
+
|
|
598
|
+
@override
|
|
599
|
+
State<${namePascal}Page> createState() => _${namePascal}PageState();
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
class _${namePascal}PageState extends State<${namePascal}Page> {
|
|
603
|
+
@override
|
|
604
|
+
Widget build(BuildContext context) {
|
|
605
|
+
return Scaffold(
|
|
606
|
+
appBar: AppBar(
|
|
607
|
+
title: const Text('${nameTitle}'),
|
|
608
|
+
),
|
|
609
|
+
body: const Center(
|
|
610
|
+
child: Text('${namePascal}Page'),
|
|
611
|
+
),
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
`;
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* 生成 Simple Stateless Page
|
|
619
|
+
*/
|
|
620
|
+
static generateSimpleStatelessPage(namePascal, nameTitle) {
|
|
621
|
+
return `import 'package:flutter/material.dart';
|
|
622
|
+
|
|
623
|
+
class ${namePascal}Page extends StatelessWidget {
|
|
624
|
+
const ${namePascal}Page({super.key});
|
|
625
|
+
|
|
626
|
+
@override
|
|
627
|
+
Widget build(BuildContext context) {
|
|
628
|
+
return Scaffold(
|
|
629
|
+
appBar: AppBar(
|
|
630
|
+
title: const Text('${nameTitle}'),
|
|
631
|
+
),
|
|
632
|
+
body: const Center(
|
|
633
|
+
child: Text('${namePascal}Page'),
|
|
634
|
+
),
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
`;
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* 生成 ViewModel
|
|
642
|
+
*/
|
|
643
|
+
static generateViewModel(namePascal, options = {}) {
|
|
644
|
+
const {
|
|
645
|
+
coreImportPath,
|
|
646
|
+
baseClass = "BaseViewModel",
|
|
647
|
+
extraImports = []
|
|
648
|
+
} = options;
|
|
649
|
+
let imports = "";
|
|
650
|
+
if (extraImports.length > 0) {
|
|
651
|
+
imports += extraImports.join("\n") + "\n";
|
|
652
|
+
}
|
|
653
|
+
if (coreImportPath) {
|
|
654
|
+
if (!extraImports.some((i) => i.includes(coreImportPath))) {
|
|
655
|
+
imports += `import '${coreImportPath}';
|
|
656
|
+
`;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return `${imports}
|
|
660
|
+
|
|
661
|
+
class ${namePascal}ViewModel extends ${baseClass} {
|
|
662
|
+
@override
|
|
663
|
+
Future<void> onInit() async {
|
|
664
|
+
await super.onInit();
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
`;
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* 生成 Service
|
|
671
|
+
*/
|
|
672
|
+
static generateService(namePascal) {
|
|
673
|
+
return `class ${namePascal}Service {
|
|
674
|
+
|
|
675
|
+
Future<void> fetchData() async {
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
`;
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* 生成 Widget (Stateful)
|
|
682
|
+
*/
|
|
683
|
+
static generateStatefulWidget(namePascal) {
|
|
684
|
+
return `import 'package:flutter/material.dart';
|
|
685
|
+
|
|
686
|
+
class ${namePascal}Widget extends StatefulWidget {
|
|
687
|
+
const ${namePascal}Widget({super.key});
|
|
688
|
+
|
|
689
|
+
@override
|
|
690
|
+
State<${namePascal}Widget> createState() => _${namePascal}WidgetState();
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
class _${namePascal}WidgetState extends State<${namePascal}Widget> {
|
|
694
|
+
@override
|
|
695
|
+
Widget build(BuildContext context) {
|
|
696
|
+
return const SizedBox.shrink();
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
`;
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* 生成 Widget (Stateless)
|
|
703
|
+
*/
|
|
704
|
+
static generateStatelessWidget(namePascal) {
|
|
705
|
+
return `import 'package:flutter/material.dart';
|
|
706
|
+
|
|
707
|
+
class ${namePascal}Widget extends StatelessWidget {
|
|
708
|
+
const ${namePascal}Widget({super.key});
|
|
709
|
+
|
|
710
|
+
@override
|
|
711
|
+
Widget build(BuildContext context) {
|
|
712
|
+
return const SizedBox.shrink();
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
`;
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* 生成 Component (Stateless)
|
|
719
|
+
*/
|
|
720
|
+
static generateComponent(namePascal) {
|
|
721
|
+
return `import 'package:flutter/material.dart';
|
|
722
|
+
|
|
723
|
+
class ${namePascal}Component extends StatelessWidget {
|
|
724
|
+
const ${namePascal}Component({super.key});
|
|
725
|
+
|
|
726
|
+
@override
|
|
727
|
+
Widget build(BuildContext context) {
|
|
728
|
+
return const SizedBox.shrink();
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
`;
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* 生成 Component (Stateful)
|
|
735
|
+
*/
|
|
736
|
+
static generateStatefulComponent(namePascal) {
|
|
737
|
+
return `import 'package:flutter/material.dart';
|
|
738
|
+
|
|
739
|
+
class ${namePascal}Component extends StatefulWidget {
|
|
740
|
+
const ${namePascal}Component({super.key});
|
|
741
|
+
|
|
742
|
+
@override
|
|
743
|
+
State<${namePascal}Component> createState() => _${namePascal}ComponentState();
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
class _${namePascal}ComponentState extends State<${namePascal}Component> {
|
|
747
|
+
@override
|
|
748
|
+
Widget build(BuildContext context) {
|
|
749
|
+
return const SizedBox.shrink();
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
`;
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* 生成 Model
|
|
756
|
+
*/
|
|
757
|
+
static generateModel(namePascal) {
|
|
758
|
+
return `class ${namePascal}Model {
|
|
759
|
+
final String id;
|
|
760
|
+
final String name;
|
|
761
|
+
|
|
762
|
+
${namePascal}Model({
|
|
763
|
+
required this.id,
|
|
764
|
+
required this.name,
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
factory ${namePascal}Model.fromJson(Map<String, dynamic> json) {
|
|
768
|
+
return ${namePascal}Model(
|
|
769
|
+
id: json['id'] as String,
|
|
770
|
+
name: json['name'] as String,
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
Map<String, dynamic> toJson() {
|
|
775
|
+
return {
|
|
776
|
+
'id': id,
|
|
777
|
+
'name': name,
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
`;
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
// src/utils/index_updater.ts
|
|
786
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
787
|
+
import { join as join4 } from "path";
|
|
788
|
+
function updateIndexFile(dirPath, fileName) {
|
|
789
|
+
try {
|
|
790
|
+
const indexPath = join4(dirPath, "index.dart");
|
|
791
|
+
if (!existsSync4(indexPath)) {
|
|
792
|
+
return false;
|
|
793
|
+
}
|
|
794
|
+
let content = readFileSync3(indexPath, "utf8");
|
|
795
|
+
const exportStatement = `export '${fileName}';`;
|
|
796
|
+
if (content.includes(exportStatement)) {
|
|
797
|
+
return false;
|
|
798
|
+
}
|
|
799
|
+
const lines = content.split("\n");
|
|
800
|
+
const comments = [];
|
|
801
|
+
const exports = [];
|
|
802
|
+
const otherLines = [];
|
|
803
|
+
for (const line of lines) {
|
|
804
|
+
const trimmed = line.trim();
|
|
805
|
+
if (trimmed.startsWith("//")) {
|
|
806
|
+
comments.push(line);
|
|
807
|
+
} else if (trimmed.startsWith("export ")) {
|
|
808
|
+
exports.push(trimmed);
|
|
809
|
+
} else if (trimmed !== "") {
|
|
810
|
+
otherLines.push(line);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
exports.push(exportStatement);
|
|
814
|
+
exports.sort();
|
|
815
|
+
const newLines = [];
|
|
816
|
+
if (comments.length > 0) {
|
|
817
|
+
newLines.push(...comments);
|
|
818
|
+
newLines.push("");
|
|
819
|
+
}
|
|
820
|
+
newLines.push(...exports);
|
|
821
|
+
if (otherLines.length > 0) {
|
|
822
|
+
newLines.push("");
|
|
823
|
+
newLines.push(...otherLines);
|
|
824
|
+
}
|
|
825
|
+
newLines.push("");
|
|
826
|
+
content = newLines.join("\n");
|
|
827
|
+
writeFileSync(indexPath, content, "utf8");
|
|
828
|
+
return true;
|
|
829
|
+
} catch (error) {
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// src/utils/template_copier.ts
|
|
835
|
+
import fsx3 from "fs-extra";
|
|
836
|
+
import { join as join6, dirname } from "path";
|
|
837
|
+
import { fileURLToPath } from "url";
|
|
838
|
+
|
|
839
|
+
// src/utils/template_renderer.ts
|
|
840
|
+
import fsx2 from "fs-extra";
|
|
841
|
+
import { join as join5 } from "path";
|
|
842
|
+
var TemplateRenderer = class {
|
|
843
|
+
/**
|
|
844
|
+
* 渲染文件内容
|
|
845
|
+
* @param content 原始字符串内容
|
|
846
|
+
* @param data 渲染上下文数据
|
|
847
|
+
*/
|
|
848
|
+
static render(content, data) {
|
|
849
|
+
let result = content;
|
|
850
|
+
const blockRegex = /[\r\n]*{{#if_(\w+)}}([\s\S]*?){{\/if_\1}}[\r\n]*/g;
|
|
851
|
+
result = result.replace(blockRegex, (match, key, blockContent) => {
|
|
852
|
+
const condition = !!data[`if_${key}`] || !!data[key];
|
|
853
|
+
const elseParts = blockContent.split(/{{else}}/);
|
|
854
|
+
if (elseParts.length > 1) {
|
|
855
|
+
return condition ? elseParts[0] : elseParts[1];
|
|
856
|
+
}
|
|
857
|
+
return condition ? blockContent : "";
|
|
858
|
+
});
|
|
859
|
+
const varRegex = /{{(\w+)}}/g;
|
|
860
|
+
result = result.replace(varRegex, (match, key) => {
|
|
861
|
+
if (data.hasOwnProperty(key)) {
|
|
862
|
+
return data[key];
|
|
863
|
+
}
|
|
864
|
+
return match;
|
|
865
|
+
});
|
|
866
|
+
return result;
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* 渲染指定文件并覆盖
|
|
870
|
+
* @param filePath 文件路径
|
|
871
|
+
* @param data 渲染上下文数据
|
|
872
|
+
*/
|
|
873
|
+
static async renderFile(filePath, data) {
|
|
874
|
+
if (!await fsx2.pathExists(filePath)) return;
|
|
875
|
+
try {
|
|
876
|
+
const content = await fsx2.readFile(filePath, "utf8");
|
|
877
|
+
const rendered = this.render(content, data);
|
|
878
|
+
await fsx2.writeFile(filePath, rendered, "utf8");
|
|
879
|
+
} catch (e) {
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* 递归渲染目录下的所有文本文件
|
|
884
|
+
* @param dir 目录路径
|
|
885
|
+
* @param data 渲染上下文数据
|
|
886
|
+
*/
|
|
887
|
+
static async renderDirectory(dir, data) {
|
|
888
|
+
if (!await fsx2.pathExists(dir)) return;
|
|
889
|
+
const items = await fsx2.readdir(dir);
|
|
890
|
+
for (const item of items) {
|
|
891
|
+
const fullPath = join5(dir, item);
|
|
892
|
+
const stat = await fsx2.stat(fullPath);
|
|
893
|
+
if (stat.isDirectory()) {
|
|
894
|
+
await this.renderDirectory(fullPath, data);
|
|
895
|
+
} else if (stat.isFile() && this.isTextFile(item)) {
|
|
896
|
+
await this.renderFile(fullPath, data);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
static isTextFile(fileName) {
|
|
901
|
+
const textExtensions = [
|
|
902
|
+
".dart",
|
|
903
|
+
".yaml",
|
|
904
|
+
".json",
|
|
905
|
+
".md",
|
|
906
|
+
".gradle",
|
|
907
|
+
".xml",
|
|
908
|
+
".plist",
|
|
909
|
+
".template"
|
|
910
|
+
];
|
|
911
|
+
return textExtensions.some((ext) => fileName.endsWith(ext));
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
// src/utils/template_copier.ts
|
|
916
|
+
function getTemplatesRootDir() {
|
|
917
|
+
const searchDirs = [];
|
|
918
|
+
if (process.env.FLU_CLI_TEMPLATES_DIR) {
|
|
919
|
+
searchDirs.push(process.env.FLU_CLI_TEMPLATES_DIR);
|
|
920
|
+
}
|
|
921
|
+
try {
|
|
922
|
+
let current = "";
|
|
923
|
+
if (typeof import.meta !== "undefined" && import.meta.url) {
|
|
924
|
+
current = dirname(fileURLToPath(import.meta.url));
|
|
925
|
+
} else if (typeof __dirname !== "undefined" && __dirname) {
|
|
926
|
+
current = __dirname;
|
|
927
|
+
}
|
|
928
|
+
if (current) {
|
|
929
|
+
let temp = current;
|
|
930
|
+
for (let i = 0; i < 6; i++) {
|
|
931
|
+
searchDirs.push(join6(temp, "packages", "core", "templates"));
|
|
932
|
+
searchDirs.push(join6(temp, "templates"));
|
|
933
|
+
searchDirs.push(join6(temp, "..", "templates"));
|
|
934
|
+
temp = dirname(temp);
|
|
935
|
+
if (temp === dirname(temp)) break;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
} catch (e) {
|
|
939
|
+
}
|
|
940
|
+
searchDirs.push(join6(process.cwd(), "templates"));
|
|
941
|
+
searchDirs.push(join6(process.cwd(), "packages", "core", "templates"));
|
|
942
|
+
for (const dir of searchDirs) {
|
|
943
|
+
try {
|
|
944
|
+
if (fsx3.pathExistsSync(dir) && fsx3.pathExistsSync(join6(dir, "core_files"))) {
|
|
945
|
+
return dir;
|
|
946
|
+
}
|
|
947
|
+
} catch {
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return join6(process.cwd(), "templates");
|
|
951
|
+
}
|
|
952
|
+
function getCoreFilesDir() {
|
|
953
|
+
return join6(getTemplatesRootDir(), "core_files");
|
|
954
|
+
}
|
|
955
|
+
async function copyCoreFiles(projectPath, options = {}) {
|
|
956
|
+
const { includeNetworkLayer = false, templateType = "lite" } = options;
|
|
957
|
+
const sourceDir = getCoreFilesDir();
|
|
958
|
+
const targetDir = join6(projectPath, "lib/core");
|
|
959
|
+
const templateTypeLower = String(templateType).toLowerCase();
|
|
960
|
+
const stateManagerLower = String(options.stateManager || "provider").toLowerCase();
|
|
961
|
+
if (!await fsx3.pathExists(sourceDir)) {
|
|
962
|
+
throw new Error(`Core files template directory not found: ${sourceDir}`);
|
|
963
|
+
}
|
|
964
|
+
await fsx3.copy(sourceDir, targetDir, {
|
|
965
|
+
overwrite: true,
|
|
966
|
+
filter: (src) => {
|
|
967
|
+
const name = src.split("/").pop() || "";
|
|
968
|
+
if (name === ".DS_Store") return false;
|
|
969
|
+
if (!includeNetworkLayer && (name === "network" || src.endsWith("/network"))) {
|
|
970
|
+
return false;
|
|
971
|
+
}
|
|
972
|
+
if (!includeNetworkLayer && name === "request_helper.dart.template") {
|
|
973
|
+
return false;
|
|
974
|
+
}
|
|
975
|
+
return true;
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
await removeTemplateSuffix(targetDir);
|
|
979
|
+
const navigatorUtilPath = join6(targetDir, "router", "navigator_util.dart");
|
|
980
|
+
const navigatorUtilMaterial = join6(targetDir, "router", "navigator_util_material.dart");
|
|
981
|
+
const navigatorUtilGetx = join6(targetDir, "router", "navigator_util_getx.dart");
|
|
982
|
+
if (stateManagerLower === "getx") {
|
|
983
|
+
if (await fsx3.pathExists(navigatorUtilGetx)) {
|
|
984
|
+
await fsx3.move(navigatorUtilGetx, navigatorUtilPath, { overwrite: true });
|
|
985
|
+
}
|
|
986
|
+
if (await fsx3.pathExists(navigatorUtilMaterial)) {
|
|
987
|
+
await fsx3.remove(navigatorUtilMaterial);
|
|
988
|
+
}
|
|
989
|
+
} else {
|
|
990
|
+
if (await fsx3.pathExists(navigatorUtilMaterial)) {
|
|
991
|
+
await fsx3.move(navigatorUtilMaterial, navigatorUtilPath, { overwrite: true });
|
|
992
|
+
}
|
|
993
|
+
if (await fsx3.pathExists(navigatorUtilGetx)) {
|
|
994
|
+
await fsx3.remove(navigatorUtilGetx);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
await TemplateRenderer.renderDirectory(targetDir, {
|
|
998
|
+
...options,
|
|
999
|
+
if_getx: stateManagerLower === "getx",
|
|
1000
|
+
if_provider: stateManagerLower === "provider",
|
|
1001
|
+
if_bloc: stateManagerLower === "bloc",
|
|
1002
|
+
if_riverpod: stateManagerLower === "riverpod",
|
|
1003
|
+
if_network: includeNetworkLayer,
|
|
1004
|
+
if_lite: templateTypeLower.includes("lite"),
|
|
1005
|
+
if_modular: templateTypeLower.includes("modular"),
|
|
1006
|
+
if_clean: templateTypeLower.includes("clean"),
|
|
1007
|
+
BASE_IMPORT: `package:${options.projectName}/core/base`,
|
|
1008
|
+
MODELS_IMPORT: `package:${options.projectName}/core/models`,
|
|
1009
|
+
SERVICES_IMPORT: `package:${options.projectName}/core/services`,
|
|
1010
|
+
VIEWMODELS_IMPORT: `package:${options.projectName}/core/viewmodels`,
|
|
1011
|
+
NETWORK_IMPORT: `package:${options.projectName}/core/network`,
|
|
1012
|
+
UTILS_IMPORT: `package:${options.projectName}/core/utils`
|
|
1013
|
+
});
|
|
1014
|
+
const filesToRemove = [
|
|
1015
|
+
join6(targetDir, "router", "router_util.dart")
|
|
1016
|
+
// 这个文件可能不再存在,但保留以防万一
|
|
1017
|
+
];
|
|
1018
|
+
for (const file of filesToRemove) {
|
|
1019
|
+
if (await fsx3.pathExists(file)) {
|
|
1020
|
+
await fsx3.remove(file);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
const appRoutesPath = join6(targetDir, "router", "app_routes.dart");
|
|
1024
|
+
if (await fsx3.pathExists(appRoutesPath)) {
|
|
1025
|
+
let content = await fsx3.readFile(appRoutesPath, "utf8");
|
|
1026
|
+
let pagesImport = "";
|
|
1027
|
+
if (templateTypeLower.includes("lite")) {
|
|
1028
|
+
pagesImport = "import '../../pages/index.dart';";
|
|
1029
|
+
} else if (templateTypeLower.includes("modular") || templateTypeLower.includes("clean")) {
|
|
1030
|
+
pagesImport = "import '../../features/index.dart';";
|
|
1031
|
+
} else {
|
|
1032
|
+
pagesImport = "import '../../pages/index.dart';";
|
|
1033
|
+
}
|
|
1034
|
+
content = content.replace("// PAGES_IMPORT_PLACEHOLDER", pagesImport);
|
|
1035
|
+
await fsx3.writeFile(appRoutesPath, content);
|
|
1036
|
+
}
|
|
1037
|
+
if (!includeNetworkLayer) {
|
|
1038
|
+
const coreIndexFile = join6(targetDir, "index.dart");
|
|
1039
|
+
if (await fsx3.pathExists(coreIndexFile)) {
|
|
1040
|
+
let content = await fsx3.readFile(coreIndexFile, "utf8");
|
|
1041
|
+
content = content.replace(/export 'network\/.*';\n/g, "");
|
|
1042
|
+
await fsx3.writeFile(coreIndexFile, content);
|
|
1043
|
+
}
|
|
1044
|
+
const utilsIndexFile = join6(targetDir, "utils", "index.dart");
|
|
1045
|
+
if (await fsx3.pathExists(utilsIndexFile)) {
|
|
1046
|
+
let content = await fsx3.readFile(utilsIndexFile, "utf8");
|
|
1047
|
+
content = content.replace(/export 'request_helper\.dart';\n?/g, "");
|
|
1048
|
+
await fsx3.writeFile(utilsIndexFile, content);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
function getNetworkDir() {
|
|
1053
|
+
return join6(getCoreFilesDir(), "network");
|
|
1054
|
+
}
|
|
1055
|
+
async function copyNetworkFiles(projectPath, options = {}) {
|
|
1056
|
+
const { targetDir = "lib/core/network", includeExamples = false } = options;
|
|
1057
|
+
const sourceDir = getNetworkDir();
|
|
1058
|
+
const destDir = join6(projectPath, targetDir);
|
|
1059
|
+
if (!await fsx3.pathExists(sourceDir)) {
|
|
1060
|
+
throw new Error(`Network template directory not found: ${sourceDir}`);
|
|
1061
|
+
}
|
|
1062
|
+
await fsx3.copy(sourceDir, destDir, {
|
|
1063
|
+
overwrite: true,
|
|
1064
|
+
filter: (src) => {
|
|
1065
|
+
const name = src.split("/").pop() || "";
|
|
1066
|
+
if (name === ".DS_Store") {
|
|
1067
|
+
return false;
|
|
1068
|
+
}
|
|
1069
|
+
if (!includeExamples) {
|
|
1070
|
+
if (name.includes("example") || name === "README.md") {
|
|
1071
|
+
return false;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
return true;
|
|
1075
|
+
}
|
|
1076
|
+
});
|
|
1077
|
+
await removeTemplateSuffix(destDir);
|
|
1078
|
+
}
|
|
1079
|
+
async function copyInfrastructure(projectPath, includeExamples = true, includeNetworkLayer = true, templateType = "lite") {
|
|
1080
|
+
await copyCoreFiles(projectPath, { includeNetworkLayer, templateType });
|
|
1081
|
+
}
|
|
1082
|
+
async function copyTemplate(templatePath, targetPath, options = {}) {
|
|
1083
|
+
try {
|
|
1084
|
+
if (!await fsx3.pathExists(templatePath)) {
|
|
1085
|
+
return false;
|
|
1086
|
+
}
|
|
1087
|
+
const defaultExcludes = [
|
|
1088
|
+
".git",
|
|
1089
|
+
".github",
|
|
1090
|
+
"node_modules",
|
|
1091
|
+
".flu-cli.yaml",
|
|
1092
|
+
"README.template.md",
|
|
1093
|
+
".DS_Store",
|
|
1094
|
+
"pubspec.yaml"
|
|
1095
|
+
];
|
|
1096
|
+
const allExcludes = [...defaultExcludes, ...options.excludes || []];
|
|
1097
|
+
await fsx3.copy(templatePath, targetPath, {
|
|
1098
|
+
overwrite: true,
|
|
1099
|
+
filter: (src) => {
|
|
1100
|
+
const relativePath = src.replace(templatePath, "");
|
|
1101
|
+
const basename = src.split(/[\\/]/).pop() || "";
|
|
1102
|
+
if (allExcludes.includes(basename)) return false;
|
|
1103
|
+
if (options.excludes && options.excludes.some((ex) => relativePath.includes(ex))) {
|
|
1104
|
+
return false;
|
|
1105
|
+
}
|
|
1106
|
+
return true;
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
const templatePubspec = join6(templatePath, "pubspec.yaml.template");
|
|
1110
|
+
const targetPubspec = join6(targetPath, "pubspec.yaml");
|
|
1111
|
+
if (await fsx3.pathExists(templatePubspec)) {
|
|
1112
|
+
await fsx3.copy(templatePubspec, targetPubspec, { overwrite: true });
|
|
1113
|
+
}
|
|
1114
|
+
await removeTemplateSuffix(targetPath);
|
|
1115
|
+
return true;
|
|
1116
|
+
} catch (error) {
|
|
1117
|
+
return false;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
async function injectNetworkExamples(projectPath, contextData = {}) {
|
|
1121
|
+
const templatesRoot = getTemplatesRootDir();
|
|
1122
|
+
const examplesDir = join6(templatesRoot, "examples");
|
|
1123
|
+
if (!await fsx3.pathExists(examplesDir)) return;
|
|
1124
|
+
const templateType = (contextData.templateType || "lite").toLowerCase();
|
|
1125
|
+
let pagesPath = "lib/pages";
|
|
1126
|
+
let vmsPath = "lib/viewmodels";
|
|
1127
|
+
let servicesPath = "lib/services";
|
|
1128
|
+
let modelsPath = "lib/models";
|
|
1129
|
+
if (templateType.includes("modular")) {
|
|
1130
|
+
pagesPath = "lib/features/home/pages";
|
|
1131
|
+
vmsPath = "lib/features/home/viewmodels";
|
|
1132
|
+
servicesPath = "lib/features/home/services";
|
|
1133
|
+
modelsPath = "lib/features/home/models";
|
|
1134
|
+
} else if (templateType.includes("clean")) {
|
|
1135
|
+
pagesPath = "lib/features/home/presentation/pages";
|
|
1136
|
+
vmsPath = "lib/features/home/presentation/viewmodels";
|
|
1137
|
+
servicesPath = "lib/features/home/data/datasources";
|
|
1138
|
+
modelsPath = "lib/features/home/data/models";
|
|
1139
|
+
}
|
|
1140
|
+
const dataLayerImport = templateType.includes("clean") ? `package:${contextData.projectName}/features/home/data/index.dart` : `package:${contextData.projectName}/${modelsPath.replace(/^lib\//, "")}/index.dart`;
|
|
1141
|
+
const renderData = {
|
|
1142
|
+
...contextData,
|
|
1143
|
+
if_getx: String(contextData.stateManager).toLowerCase() === "getx",
|
|
1144
|
+
if_provider: String(contextData.stateManager).toLowerCase() === "provider",
|
|
1145
|
+
if_bloc: String(contextData.stateManager).toLowerCase() === "bloc",
|
|
1146
|
+
if_riverpod: String(contextData.stateManager).toLowerCase() === "riverpod",
|
|
1147
|
+
if_clean: templateType.includes("clean"),
|
|
1148
|
+
CORE_IMPORT: `package:${contextData.projectName}/core/index.dart`,
|
|
1149
|
+
BASE_IMPORT: `package:${contextData.projectName}/core/base/index.dart`,
|
|
1150
|
+
CONFIG_IMPORT: `package:${contextData.projectName}/core/config/index.dart`,
|
|
1151
|
+
// ✅ 复用 dataLayerImport 变量
|
|
1152
|
+
DATA_IMPORT: dataLayerImport,
|
|
1153
|
+
MODELS_IMPORT: dataLayerImport,
|
|
1154
|
+
SERVICES_IMPORT: templateType.includes("clean") ? dataLayerImport : `package:${contextData.projectName}/${servicesPath.replace(/^lib\//, "")}/index.dart`,
|
|
1155
|
+
VIEWMODELS_IMPORT: `package:${contextData.projectName}/${vmsPath.replace(/^lib\//, "")}/index.dart`,
|
|
1156
|
+
NETWORK_IMPORT: `package:${contextData.projectName}/core/network/index.dart`,
|
|
1157
|
+
UTILS_IMPORT: `package:${contextData.projectName}/core/utils/index.dart`
|
|
1158
|
+
};
|
|
1159
|
+
const copyAndExport = async (srcFile, targetSubDir, targetFileName) => {
|
|
1160
|
+
const srcPath = join6(examplesDir, srcFile);
|
|
1161
|
+
const targetDir = join6(projectPath, targetSubDir);
|
|
1162
|
+
const targetPath = join6(targetDir, targetFileName);
|
|
1163
|
+
if (await fsx3.pathExists(srcPath)) {
|
|
1164
|
+
await fsx3.ensureDir(targetDir);
|
|
1165
|
+
await fsx3.copy(srcPath, targetPath);
|
|
1166
|
+
let finalPath = targetPath;
|
|
1167
|
+
if (targetPath.endsWith(".template")) {
|
|
1168
|
+
finalPath = targetPath.replace(/\.template$/, "");
|
|
1169
|
+
await fsx3.rename(targetPath, finalPath);
|
|
1170
|
+
}
|
|
1171
|
+
await TemplateRenderer.renderFile(finalPath, renderData);
|
|
1172
|
+
}
|
|
1173
|
+
};
|
|
1174
|
+
await copyAndExport("eg_list_page.dart.template", pagesPath, "eg_list_page.dart.template");
|
|
1175
|
+
await copyAndExport("eg_list_viewmodel.dart.template", vmsPath, "eg_list_viewmodel.dart.template");
|
|
1176
|
+
await copyAndExport("eg_service.dart.template", servicesPath, "eg_service.dart.template");
|
|
1177
|
+
await copyAndExport("tu_chong_model.dart.template", modelsPath, "tu_chong_model.dart.template");
|
|
1178
|
+
await copyAndExport("mock_data.dart.template", modelsPath, "mock_data.dart.template");
|
|
1179
|
+
await generateIndexFile(join6(projectPath, pagesPath));
|
|
1180
|
+
await generateIndexFile(join6(projectPath, vmsPath));
|
|
1181
|
+
await generateIndexFile(join6(projectPath, servicesPath));
|
|
1182
|
+
await generateIndexFile(join6(projectPath, modelsPath));
|
|
1183
|
+
}
|
|
1184
|
+
async function generateIndexFile(dir) {
|
|
1185
|
+
if (!await fsx3.pathExists(dir)) return;
|
|
1186
|
+
const files = await fsx3.readdir(dir);
|
|
1187
|
+
const exports = [];
|
|
1188
|
+
for (const file of files) {
|
|
1189
|
+
if (file.endsWith(".dart") && file !== "index.dart" && !file.includes(".template")) {
|
|
1190
|
+
const content = await fsx3.readFile(join6(dir, file), "utf8");
|
|
1191
|
+
if (!content.trim().startsWith("part of")) {
|
|
1192
|
+
exports.push(`export '${file}';`);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
if (exports.length > 0) {
|
|
1197
|
+
const indexContent = exports.sort().join("\n") + "\n";
|
|
1198
|
+
await fsx3.writeFile(join6(dir, "index.dart"), indexContent);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
async function copyCustomTemplate(templatePath, targetPath) {
|
|
1202
|
+
try {
|
|
1203
|
+
if (!await fsx3.pathExists(templatePath)) {
|
|
1204
|
+
return false;
|
|
1205
|
+
}
|
|
1206
|
+
const includeDirs = ["lib", "assets", ".vscode"];
|
|
1207
|
+
const includeFiles = [
|
|
1208
|
+
"analysis_options.yaml",
|
|
1209
|
+
"README.md",
|
|
1210
|
+
"pubspec.yaml"
|
|
1211
|
+
];
|
|
1212
|
+
for (const dir of includeDirs) {
|
|
1213
|
+
const srcDir = join6(templatePath, dir);
|
|
1214
|
+
const dstDir = join6(targetPath, dir);
|
|
1215
|
+
if (await fsx3.pathExists(srcDir)) {
|
|
1216
|
+
await fsx3.copy(srcDir, dstDir, { overwrite: true });
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
for (const file of includeFiles) {
|
|
1220
|
+
const srcFile = join6(templatePath, file);
|
|
1221
|
+
const dstFile = join6(targetPath, file);
|
|
1222
|
+
if (await fsx3.pathExists(srcFile)) {
|
|
1223
|
+
await fsx3.copy(srcFile, dstFile, { overwrite: true });
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
await removeTemplateSuffix(targetPath);
|
|
1227
|
+
return true;
|
|
1228
|
+
} catch (error) {
|
|
1229
|
+
return false;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
async function replaceVariables(projectDir, variables) {
|
|
1233
|
+
try {
|
|
1234
|
+
const replacements = {
|
|
1235
|
+
"{{projectName}}": variables.projectName,
|
|
1236
|
+
"{{project_name}}": variables.projectName,
|
|
1237
|
+
"{{package_name}}": variables.packageName,
|
|
1238
|
+
"{{author}}": variables.author || "Your Name",
|
|
1239
|
+
"{{year}}": (/* @__PURE__ */ new Date()).getFullYear().toString()
|
|
1240
|
+
};
|
|
1241
|
+
await replaceInDirectory(projectDir, replacements);
|
|
1242
|
+
return true;
|
|
1243
|
+
} catch (error) {
|
|
1244
|
+
return false;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
async function replaceInDirectory(dir, replacements) {
|
|
1248
|
+
const items = await fsx3.readdir(dir);
|
|
1249
|
+
for (const item of items) {
|
|
1250
|
+
const itemPath = join6(dir, item);
|
|
1251
|
+
const stat = await fsx3.stat(itemPath);
|
|
1252
|
+
if (stat.isDirectory()) {
|
|
1253
|
+
if ([".git", "node_modules", ".dart_tool", "build"].includes(item)) {
|
|
1254
|
+
continue;
|
|
1255
|
+
}
|
|
1256
|
+
await replaceInDirectory(itemPath, replacements);
|
|
1257
|
+
} else if (stat.isFile()) {
|
|
1258
|
+
if (isTextFile(itemPath)) {
|
|
1259
|
+
await replaceInFile(itemPath, replacements);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
async function replaceInFile(filePath, replacements) {
|
|
1265
|
+
try {
|
|
1266
|
+
let content = await fsx3.readFile(filePath, "utf8");
|
|
1267
|
+
let modified = false;
|
|
1268
|
+
for (const [pattern, value] of Object.entries(replacements)) {
|
|
1269
|
+
if (content.includes(pattern)) {
|
|
1270
|
+
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1271
|
+
content = content.replace(new RegExp(escaped, "g"), value);
|
|
1272
|
+
modified = true;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
if (modified) {
|
|
1276
|
+
await fsx3.writeFile(filePath, content, "utf8");
|
|
1277
|
+
}
|
|
1278
|
+
} catch (e) {
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
function isTextFile(filePath) {
|
|
1282
|
+
const textExtensions = [
|
|
1283
|
+
".dart",
|
|
1284
|
+
".yaml",
|
|
1285
|
+
".yml",
|
|
1286
|
+
".json",
|
|
1287
|
+
".md",
|
|
1288
|
+
".txt",
|
|
1289
|
+
".gradle",
|
|
1290
|
+
".xml",
|
|
1291
|
+
".plist",
|
|
1292
|
+
".swift",
|
|
1293
|
+
".kt",
|
|
1294
|
+
".java",
|
|
1295
|
+
".js",
|
|
1296
|
+
".ts",
|
|
1297
|
+
".html",
|
|
1298
|
+
".css",
|
|
1299
|
+
".sh"
|
|
1300
|
+
];
|
|
1301
|
+
return textExtensions.some((ext) => filePath.endsWith(ext));
|
|
1302
|
+
}
|
|
1303
|
+
async function removeTemplateSuffix(dir) {
|
|
1304
|
+
if (!await fsx3.pathExists(dir)) {
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
const items = await fsx3.readdir(dir);
|
|
1308
|
+
for (const item of items) {
|
|
1309
|
+
const itemPath = join6(dir, item);
|
|
1310
|
+
const stat = await fsx3.stat(itemPath);
|
|
1311
|
+
if (stat.isDirectory()) {
|
|
1312
|
+
await removeTemplateSuffix(itemPath);
|
|
1313
|
+
} else if (item.endsWith(".template")) {
|
|
1314
|
+
const newPath = itemPath.replace(/\.template$/, "");
|
|
1315
|
+
await fsx3.rename(itemPath, newPath);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
async function ensurePubspecName(projectDir, projectName) {
|
|
1320
|
+
try {
|
|
1321
|
+
const pubspec = join6(projectDir, "pubspec.yaml");
|
|
1322
|
+
if (!await fsx3.pathExists(pubspec)) return;
|
|
1323
|
+
let content = await fsx3.readFile(pubspec, "utf8");
|
|
1324
|
+
if (/^name:\s+/m.test(content)) {
|
|
1325
|
+
content = content.replace(/^name:\s+.*/m, `name: ${projectName}`);
|
|
1326
|
+
} else {
|
|
1327
|
+
content = `name: ${projectName}
|
|
1328
|
+
` + content;
|
|
1329
|
+
}
|
|
1330
|
+
await fsx3.writeFile(pubspec, content, "utf8");
|
|
1331
|
+
} catch (error) {
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
async function cleanupTemplateFiles(projectDir) {
|
|
1335
|
+
const filesToRemove = [".flu-cli.yaml", "README.template.md", ".git"];
|
|
1336
|
+
for (const file of filesToRemove) {
|
|
1337
|
+
const filePath = join6(projectDir, file);
|
|
1338
|
+
if (await fsx3.pathExists(filePath)) {
|
|
1339
|
+
await fsx3.remove(filePath);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// src/generators/page_generator.ts
|
|
1345
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
1346
|
+
import { join as join9, relative as relative2, dirname as dirname3 } from "path";
|
|
1347
|
+
|
|
1348
|
+
// src/generators/model_generator.ts
|
|
1349
|
+
import { existsSync as existsSync5, mkdirSync, writeFileSync as writeFileSync2, readFileSync as readFileSync4 } from "fs";
|
|
1350
|
+
import { join as join7 } from "path";
|
|
1351
|
+
|
|
1352
|
+
// src/templates/simple_templates.ts
|
|
1353
|
+
function getSimpleViewModel(namePascal) {
|
|
1354
|
+
return `import 'package:flutter/foundation.dart';
|
|
1355
|
+
|
|
1356
|
+
class ${namePascal}ViewModel extends ChangeNotifier {
|
|
1357
|
+
bool _isLoading = false;
|
|
1358
|
+
bool get isLoading => _isLoading;
|
|
1359
|
+
|
|
1360
|
+
void setLoading(bool loading) {
|
|
1361
|
+
_isLoading = loading;
|
|
1362
|
+
notifyListeners();
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// TODO: Add your logic here
|
|
1366
|
+
}
|
|
1367
|
+
`;
|
|
1368
|
+
}
|
|
1369
|
+
function getSimpleWidget(namePascal, isStateful = false) {
|
|
1370
|
+
if (isStateful) {
|
|
1371
|
+
return `import 'package:flutter/material.dart';
|
|
1372
|
+
|
|
1373
|
+
class ${namePascal} extends StatefulWidget {
|
|
1374
|
+
const ${namePascal}({super.key});
|
|
1375
|
+
|
|
1376
|
+
@override
|
|
1377
|
+
State<${namePascal}> createState() => _${namePascal}State();
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
class _${namePascal}State extends State<${namePascal}> {
|
|
1381
|
+
@override
|
|
1382
|
+
Widget build(BuildContext context) {
|
|
1383
|
+
return const SizedBox.shrink();
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
`;
|
|
1387
|
+
}
|
|
1388
|
+
return `import 'package:flutter/material.dart';
|
|
1389
|
+
|
|
1390
|
+
class ${namePascal} extends StatelessWidget {
|
|
1391
|
+
const ${namePascal}({super.key});
|
|
1392
|
+
|
|
1393
|
+
@override
|
|
1394
|
+
Widget build(BuildContext context) {
|
|
1395
|
+
return const SizedBox.shrink();
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
`;
|
|
1399
|
+
}
|
|
1400
|
+
function getSimpleModel(namePascal) {
|
|
1401
|
+
return `class ${namePascal} {
|
|
1402
|
+
// TODO: Add properties
|
|
1403
|
+
|
|
1404
|
+
${namePascal}();
|
|
1405
|
+
|
|
1406
|
+
factory ${namePascal}.fromJson(Map<String, dynamic> json) {
|
|
1407
|
+
return ${namePascal}(
|
|
1408
|
+
// TODO: Map json to properties
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
Map<String, dynamic> toJson() {
|
|
1413
|
+
return {
|
|
1414
|
+
// TODO: Map properties to json
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
`;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// src/generators/model_generator.ts
|
|
1422
|
+
async function generateModel(name, options = {}, logger2 = new ConsoleLogger()) {
|
|
1423
|
+
try {
|
|
1424
|
+
const {
|
|
1425
|
+
feature = null,
|
|
1426
|
+
jsonFile = null,
|
|
1427
|
+
jsonData = null,
|
|
1428
|
+
// 新增:支持直接传入 JSON 对象
|
|
1429
|
+
outputDir = process.cwd()
|
|
1430
|
+
} = options;
|
|
1431
|
+
const projectConfig = ProjectConfigManager.loadConfig(outputDir);
|
|
1432
|
+
const modelConfig = projectConfig?.generators?.model;
|
|
1433
|
+
const namePascal = toPascalCase(name);
|
|
1434
|
+
const nameSnake = toSnakeCase(name);
|
|
1435
|
+
let modelsDir;
|
|
1436
|
+
if (modelConfig?.path) {
|
|
1437
|
+
modelsDir = join7(outputDir, modelConfig.path.replace("{feature}", feature || name));
|
|
1438
|
+
} else {
|
|
1439
|
+
modelsDir = getModelPath(outputDir, feature);
|
|
1440
|
+
}
|
|
1441
|
+
if (!existsSync5(modelsDir)) {
|
|
1442
|
+
mkdirSync(modelsDir, { recursive: true });
|
|
1443
|
+
}
|
|
1444
|
+
let content = null;
|
|
1445
|
+
const isCustom = isCustomTemplateProject(outputDir);
|
|
1446
|
+
if (jsonData) {
|
|
1447
|
+
logger2.info("\u4F7F\u7528\u4F20\u5165\u7684 JSON \u6570\u636E\u751F\u6210\u6A21\u578B");
|
|
1448
|
+
content = generateModelFromJson(namePascal, jsonData);
|
|
1449
|
+
} else if (jsonFile && existsSync5(jsonFile)) {
|
|
1450
|
+
logger2.info(`\u4ECE JSON \u6587\u4EF6\u751F\u6210: ${jsonFile}`);
|
|
1451
|
+
const jsonContent = readFileSync4(jsonFile, "utf8");
|
|
1452
|
+
const parsedData = JSON.parse(jsonContent);
|
|
1453
|
+
content = generateModelFromJson(namePascal, parsedData);
|
|
1454
|
+
} else {
|
|
1455
|
+
if (modelConfig) {
|
|
1456
|
+
if (!modelConfig.withBaseModel) {
|
|
1457
|
+
content = getSimpleModel(namePascal);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
if (!content && !modelConfig && isCustom) {
|
|
1461
|
+
content = getSimpleModel(namePascal);
|
|
1462
|
+
}
|
|
1463
|
+
if (!content) {
|
|
1464
|
+
content = getSnippetContent(outputDir, "flu.model", { Name: namePascal }) || generateBasicModel(namePascal);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
const fileName = modelConfig?.fileName ? modelConfig.fileName.replace("{name}", nameSnake) : `${nameSnake}${modelConfig?.fileSuffix ?? "_model"}.dart`;
|
|
1468
|
+
const filePath = join7(modelsDir, fileName);
|
|
1469
|
+
if (existsSync5(filePath)) {
|
|
1470
|
+
logger2.error(`\u6587\u4EF6\u5DF2\u5B58\u5728: ${filePath}`);
|
|
1471
|
+
return false;
|
|
1472
|
+
}
|
|
1473
|
+
writeFileSync2(filePath, content, "utf8");
|
|
1474
|
+
logger2.success(`Model \u521B\u5EFA\u6210\u529F: ${filePath}`);
|
|
1475
|
+
updateIndexFile(modelsDir, fileName);
|
|
1476
|
+
return true;
|
|
1477
|
+
} catch (error) {
|
|
1478
|
+
logger2.error(`\u751F\u6210 Model \u5931\u8D25: ${error.message}`);
|
|
1479
|
+
return false;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
function generateBasicModel(namePascal) {
|
|
1483
|
+
return `class ${namePascal}Model {
|
|
1484
|
+
final String id;
|
|
1485
|
+
final String name;
|
|
1486
|
+
final DateTime createdAt;
|
|
1487
|
+
|
|
1488
|
+
${namePascal}Model({
|
|
1489
|
+
required this.id,
|
|
1490
|
+
required this.name,
|
|
1491
|
+
required this.createdAt,
|
|
1492
|
+
});
|
|
1493
|
+
|
|
1494
|
+
// \u4ECE JSON \u521B\u5EFA
|
|
1495
|
+
factory ${namePascal}Model.fromJson(Map<String, dynamic> json) {
|
|
1496
|
+
return ${namePascal}Model(
|
|
1497
|
+
id: json['id'] as String,
|
|
1498
|
+
name: json['name'] as String,
|
|
1499
|
+
createdAt: DateTime.parse(json['created_at'] as String),
|
|
1500
|
+
);
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
// \u8F6C\u6362\u4E3A JSON
|
|
1504
|
+
Map<String, dynamic> toJson() {
|
|
1505
|
+
return {
|
|
1506
|
+
'id': id,
|
|
1507
|
+
'name': name,
|
|
1508
|
+
'created_at': createdAt.toIso8601String(),
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
// \u590D\u5236\u5E76\u4FEE\u6539
|
|
1513
|
+
${namePascal}Model copyWith({
|
|
1514
|
+
String? id,
|
|
1515
|
+
String? name,
|
|
1516
|
+
DateTime? createdAt,
|
|
1517
|
+
}) {
|
|
1518
|
+
return ${namePascal}Model(
|
|
1519
|
+
id: id ?? this.id,
|
|
1520
|
+
name: name ?? this.name,
|
|
1521
|
+
createdAt: createdAt ?? this.createdAt,
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
@override
|
|
1526
|
+
String toString() {
|
|
1527
|
+
return '${namePascal}Model(id: $id, name: $name, createdAt: $createdAt)';
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
@override
|
|
1531
|
+
bool operator ==(Object other) {
|
|
1532
|
+
if (identical(this, other)) return true;
|
|
1533
|
+
return other is ${namePascal}Model &&
|
|
1534
|
+
other.id == id &&
|
|
1535
|
+
other.name == name &&
|
|
1536
|
+
other.createdAt == createdAt;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
@override
|
|
1540
|
+
int get hashCode {
|
|
1541
|
+
return id.hashCode ^ name.hashCode ^ createdAt.hashCode;
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
`;
|
|
1545
|
+
}
|
|
1546
|
+
function generateModelFromJson(namePascal, jsonData) {
|
|
1547
|
+
const fields = analyzeJsonStructure(jsonData);
|
|
1548
|
+
const fieldDeclarations = fields.map((f) => ` final ${f.type} ${f.name};`).join("\n");
|
|
1549
|
+
const constructorParams = fields.map((f) => ` required this.${f.name},`).join("\n");
|
|
1550
|
+
const fromJsonFields = fields.map((f) => {
|
|
1551
|
+
if (f.type === "String") {
|
|
1552
|
+
return ` ${f.name}: (json['${f.jsonKey}'] as String?) ?? '',`;
|
|
1553
|
+
} else if (f.type === "int") {
|
|
1554
|
+
return ` ${f.name}: (json['${f.jsonKey}'] as num?)?.toInt() ?? 0,`;
|
|
1555
|
+
} else if (f.type === "double") {
|
|
1556
|
+
return ` ${f.name}: (json['${f.jsonKey}'] as num?)?.toDouble() ?? 0.0,`;
|
|
1557
|
+
} else if (f.type === "bool") {
|
|
1558
|
+
return ` ${f.name}: (json['${f.jsonKey}'] as bool?) ?? false,`;
|
|
1559
|
+
} else if (f.type === "DateTime") {
|
|
1560
|
+
return ` ${f.name}: DateTime.tryParse((json['${f.jsonKey}'] as String?) ?? '') ?? DateTime.now(),`;
|
|
1561
|
+
} else if (f.type.startsWith("List<")) {
|
|
1562
|
+
const itemType = f.type.slice(5, -1);
|
|
1563
|
+
if (["String", "int", "double", "bool"].includes(itemType)) {
|
|
1564
|
+
return ` ${f.name}: (json['${f.jsonKey}'] as List<dynamic>?)?.map((e) => e as ${itemType}).toList() ?? [],`;
|
|
1565
|
+
}
|
|
1566
|
+
return ` ${f.name}: (json['${f.jsonKey}'] as List<dynamic>?)?.cast<${itemType}>() ?? [],`;
|
|
1567
|
+
} else {
|
|
1568
|
+
return ` ${f.name}: json['${f.jsonKey}'],`;
|
|
1569
|
+
}
|
|
1570
|
+
}).join("\n");
|
|
1571
|
+
const toJsonFields = fields.map((f) => {
|
|
1572
|
+
if (f.type === "DateTime") {
|
|
1573
|
+
return ` '${f.jsonKey}': ${f.name}.toIso8601String(),`;
|
|
1574
|
+
} else {
|
|
1575
|
+
return ` '${f.jsonKey}': ${f.name},`;
|
|
1576
|
+
}
|
|
1577
|
+
}).join("\n");
|
|
1578
|
+
const copyWithParams = fields.map((f) => ` ${f.type}? ${f.name},`).join("\n");
|
|
1579
|
+
const copyWithFields = fields.map((f) => ` ${f.name}: ${f.name} ?? this.${f.name},`).join("\n");
|
|
1580
|
+
const toStringFields = fields.map((f) => `${f.name}: $${f.name}`).join(", ");
|
|
1581
|
+
const equalityChecks = fields.map((f) => ` other.${f.name} == ${f.name}`).join(" &&\n");
|
|
1582
|
+
const hashCodeFields = fields.map((f) => `${f.name}.hashCode`).join(" ^ ");
|
|
1583
|
+
return `class ${namePascal}Model {
|
|
1584
|
+
${fieldDeclarations}
|
|
1585
|
+
|
|
1586
|
+
${namePascal}Model({
|
|
1587
|
+
${constructorParams}
|
|
1588
|
+
});
|
|
1589
|
+
|
|
1590
|
+
// \u4ECE JSON \u521B\u5EFA
|
|
1591
|
+
factory ${namePascal}Model.fromJson(Map<String, dynamic> json) {
|
|
1592
|
+
return ${namePascal}Model(
|
|
1593
|
+
${fromJsonFields}
|
|
1594
|
+
);
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
// \u8F6C\u6362\u4E3A JSON
|
|
1598
|
+
Map<String, dynamic> toJson() {
|
|
1599
|
+
return {
|
|
1600
|
+
${toJsonFields}
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// \u590D\u5236\u5E76\u4FEE\u6539
|
|
1605
|
+
${namePascal}Model copyWith({
|
|
1606
|
+
${copyWithParams}
|
|
1607
|
+
}) {
|
|
1608
|
+
return ${namePascal}Model(
|
|
1609
|
+
${copyWithFields}
|
|
1610
|
+
);
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
@override
|
|
1614
|
+
String toString() {
|
|
1615
|
+
return '${namePascal}Model(${toStringFields})';
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
@override
|
|
1619
|
+
bool operator ==(Object other) {
|
|
1620
|
+
if (identical(this, other)) return true;
|
|
1621
|
+
return other is ${namePascal}Model &&
|
|
1622
|
+
${equalityChecks};
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
@override
|
|
1626
|
+
int get hashCode {
|
|
1627
|
+
return ${hashCodeFields};
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
`;
|
|
1631
|
+
}
|
|
1632
|
+
function analyzeJsonStructure(jsonData) {
|
|
1633
|
+
const fields = [];
|
|
1634
|
+
for (const [key, value] of Object.entries(jsonData)) {
|
|
1635
|
+
const camelKey = toCamelCase(key);
|
|
1636
|
+
const type = inferDartType(value);
|
|
1637
|
+
fields.push({
|
|
1638
|
+
name: camelKey,
|
|
1639
|
+
jsonKey: key,
|
|
1640
|
+
type
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
return fields;
|
|
1644
|
+
}
|
|
1645
|
+
function inferDartType(value) {
|
|
1646
|
+
if (value === null) return "dynamic";
|
|
1647
|
+
const type = typeof value;
|
|
1648
|
+
if (type === "string") {
|
|
1649
|
+
if (/^\d{4}-\d{2}-\d{2}/.test(value)) {
|
|
1650
|
+
return "DateTime";
|
|
1651
|
+
}
|
|
1652
|
+
return "String";
|
|
1653
|
+
}
|
|
1654
|
+
if (type === "number") {
|
|
1655
|
+
return Number.isInteger(value) ? "int" : "double";
|
|
1656
|
+
}
|
|
1657
|
+
if (type === "boolean") {
|
|
1658
|
+
return "bool";
|
|
1659
|
+
}
|
|
1660
|
+
if (Array.isArray(value)) {
|
|
1661
|
+
if (value.length === 0) {
|
|
1662
|
+
return "List<dynamic>";
|
|
1663
|
+
}
|
|
1664
|
+
const itemType = inferDartType(value[0]);
|
|
1665
|
+
return `List<${itemType}>`;
|
|
1666
|
+
}
|
|
1667
|
+
if (type === "object") {
|
|
1668
|
+
return "Map<String, dynamic>";
|
|
1669
|
+
}
|
|
1670
|
+
return "dynamic";
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// src/generators/viewmodel_generator.ts
|
|
1674
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
1675
|
+
import { join as join8, relative, dirname as dirname2 } from "path";
|
|
1676
|
+
async function generateViewModel(name, options = {}, logger2 = new ConsoleLogger()) {
|
|
1677
|
+
try {
|
|
1678
|
+
const { feature = null, outputDir = process.cwd() } = options;
|
|
1679
|
+
const namePascal = toPascalCase(name);
|
|
1680
|
+
const nameSnake = toSnakeCase(name);
|
|
1681
|
+
const projectConfig = ProjectConfigManager.loadConfig(outputDir);
|
|
1682
|
+
const vmConfig = projectConfig?.generators?.viewModel;
|
|
1683
|
+
let vmDir;
|
|
1684
|
+
if (vmConfig?.path) {
|
|
1685
|
+
vmDir = join8(
|
|
1686
|
+
outputDir,
|
|
1687
|
+
vmConfig.path.replace("{feature}", feature || name)
|
|
1688
|
+
);
|
|
1689
|
+
} else {
|
|
1690
|
+
vmDir = getViewModelPath(outputDir, feature || name);
|
|
1691
|
+
}
|
|
1692
|
+
if (!existsSync6(vmDir)) {
|
|
1693
|
+
mkdirSync2(vmDir, { recursive: true });
|
|
1694
|
+
}
|
|
1695
|
+
const template = options.template || detectProjectTemplate(outputDir) || "lite";
|
|
1696
|
+
const stateManager = options.stateManager || getStateManager(outputDir);
|
|
1697
|
+
const isCustom = isCustomTemplateProject(outputDir);
|
|
1698
|
+
let content = null;
|
|
1699
|
+
if (vmConfig) {
|
|
1700
|
+
if (!vmConfig.withBaseViewModel) {
|
|
1701
|
+
content = getSimpleViewModel(namePascal);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
if (!content && !vmConfig) {
|
|
1705
|
+
if (isCustom && !["lite", "modular", "clean"].includes(template)) {
|
|
1706
|
+
content = getSimpleViewModel(namePascal);
|
|
1707
|
+
logger2.info("\u751F\u6210\u7C7B\u578B: Simple ViewModel (\u672A\u77E5/\u81EA\u5B9A\u4E49\u6A21\u677F)");
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
if (!content) {
|
|
1711
|
+
const fileName2 = vmConfig?.fileName ? vmConfig.fileName.replace("{name}", nameSnake) : `${nameSnake}${vmConfig?.fileSuffix ?? "_viewmodel"}.dart`;
|
|
1712
|
+
const filePath2 = join8(vmDir, fileName2);
|
|
1713
|
+
const { StateManagerAdapterFactory: StateManagerAdapterFactory2 } = await import("./factory-6DDXZYQP.js");
|
|
1714
|
+
const adapter = StateManagerAdapterFactory2.getAdapter(stateManager);
|
|
1715
|
+
const coreImportPath = getPackageImportPath(outputDir, "core/index.dart") || calculateRelativeImport(filePath2, join8(outputDir, "lib", "core", "index.dart"));
|
|
1716
|
+
const variables = {
|
|
1717
|
+
Name: namePascal,
|
|
1718
|
+
name: nameSnake.replace(/_([a-z])/g, (_, c) => c.toUpperCase()),
|
|
1719
|
+
snake_name: nameSnake,
|
|
1720
|
+
relative_core_path: coreImportPath,
|
|
1721
|
+
base_viewmodel: adapter.getBaseViewModelParent?.(options.isListPage || false) || "BaseViewModel"
|
|
1722
|
+
};
|
|
1723
|
+
let key = stateManager === "riverpod" ? "flu.riverpodVm" : "flu.vm";
|
|
1724
|
+
content = getSnippetContent(outputDir, key, variables);
|
|
1725
|
+
if (!content) {
|
|
1726
|
+
const imports = adapter.getImports?.(coreImportPath) || [`import '${coreImportPath}';`];
|
|
1727
|
+
const baseClass = adapter.getBaseViewModelParent?.(options.isListPage || false) || "BaseViewModel";
|
|
1728
|
+
if (stateManager === "riverpod") {
|
|
1729
|
+
const stateClass = `${namePascal}State`;
|
|
1730
|
+
const camel = nameSnake.replace(/_([a-z])/g, (_, $1) => $1.toUpperCase());
|
|
1731
|
+
const providerName = `${camel}Provider`;
|
|
1732
|
+
const baseStatePath = coreImportPath.startsWith("package:") ? coreImportPath.replace("index.dart", "core/base/base_state.dart") : coreImportPath.replace("index.dart", "core/base/base_state.dart");
|
|
1733
|
+
const baseNotifierPath = coreImportPath.startsWith("package:") ? coreImportPath.replace("index.dart", "core/base/base_notifier.dart") : coreImportPath.replace("index.dart", "core/base/base_notifier.dart");
|
|
1734
|
+
content = `import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
1735
|
+
import '${baseStatePath}';
|
|
1736
|
+
import '${baseNotifierPath}';
|
|
1737
|
+
|
|
1738
|
+
class ${stateClass} extends BaseState {
|
|
1739
|
+
final int counter;
|
|
1740
|
+
const ${stateClass}({ ViewState state = ViewState.idle, bool isRefreshing = false, String? error, this.counter = 0 })
|
|
1741
|
+
: super(state: state, isRefreshing: isRefreshing, error: error);
|
|
1742
|
+
|
|
1743
|
+
${stateClass} copy({ ViewState? state, bool? isRefreshing, String? error, int? counter }) {
|
|
1744
|
+
return ${stateClass}(
|
|
1745
|
+
state: state ?? this.state,
|
|
1746
|
+
isRefreshing: isRefreshing ?? this.isRefreshing,
|
|
1747
|
+
error: error ?? this.error,
|
|
1748
|
+
counter: counter ?? this.counter,
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
class ${namePascal}ViewModel extends BaseNotifier<${stateClass}> {
|
|
1754
|
+
@override
|
|
1755
|
+
${stateClass} createInitialState() => const ${stateClass}();
|
|
1756
|
+
|
|
1757
|
+
Future<void> loadData() async {
|
|
1758
|
+
await run(() async {
|
|
1759
|
+
await Future.delayed(const Duration(milliseconds: 500));
|
|
1760
|
+
state = state.copy(counter: state.counter + 1);
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
Future<void> refresh() async {
|
|
1765
|
+
setRefreshing(true);
|
|
1766
|
+
await loadData();
|
|
1767
|
+
setRefreshing(false);
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
final ${providerName} = NotifierProvider<${namePascal}ViewModel, ${stateClass}>(${namePascal}ViewModel.new);
|
|
1772
|
+
`;
|
|
1773
|
+
} else {
|
|
1774
|
+
content = TemplateGenerator.generateViewModel(namePascal, {
|
|
1775
|
+
coreImportPath,
|
|
1776
|
+
baseClass,
|
|
1777
|
+
extraImports: imports
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
const fileName = vmConfig?.fileName ? vmConfig.fileName.replace("{name}", nameSnake) : `${nameSnake}${vmConfig?.fileSuffix ?? "_viewmodel"}.dart`;
|
|
1783
|
+
const filePath = join8(vmDir, fileName);
|
|
1784
|
+
if (existsSync6(filePath)) {
|
|
1785
|
+
logger2.error(`\u6587\u4EF6\u5DF2\u5B58\u5728: ${filePath}`);
|
|
1786
|
+
return false;
|
|
1787
|
+
}
|
|
1788
|
+
if (content === null) {
|
|
1789
|
+
logger2.error("\u65E0\u6CD5\u751F\u6210 ViewModel \u5185\u5BB9");
|
|
1790
|
+
return false;
|
|
1791
|
+
}
|
|
1792
|
+
writeFileSync3(filePath, content, "utf8");
|
|
1793
|
+
logger2.success(`ViewModel \u521B\u5EFA\u6210\u529F: ${filePath}`);
|
|
1794
|
+
updateIndexFile(vmDir, fileName);
|
|
1795
|
+
return true;
|
|
1796
|
+
} catch (error) {
|
|
1797
|
+
logger2.error(`\u751F\u6210 ViewModel \u5931\u8D25: ${error.message}`);
|
|
1798
|
+
return false;
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
function calculateRelativeImport(fromFile, toFile) {
|
|
1802
|
+
const fromDir = dirname2(fromFile);
|
|
1803
|
+
const relPath = relative(fromDir, toFile);
|
|
1804
|
+
return relPath.replace(/\\/g, "/");
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
// src/generators/page_generator.ts
|
|
1808
|
+
async function generatePage(name, options = {}, logger2 = new ConsoleLogger()) {
|
|
1809
|
+
try {
|
|
1810
|
+
const {
|
|
1811
|
+
feature = null,
|
|
1812
|
+
stateful = false,
|
|
1813
|
+
stateless = false,
|
|
1814
|
+
withViewModel = true,
|
|
1815
|
+
isListPage = false,
|
|
1816
|
+
outputDir = process.cwd()
|
|
1817
|
+
} = options;
|
|
1818
|
+
const projectConfig = ProjectConfigManager.loadConfig(outputDir);
|
|
1819
|
+
const pageConfig = projectConfig?.generators?.page;
|
|
1820
|
+
const template = detectProjectTemplate(outputDir);
|
|
1821
|
+
const stateManager = getStateManager(outputDir);
|
|
1822
|
+
const isCustom = isCustomTemplateProject(outputDir);
|
|
1823
|
+
logger2.info(
|
|
1824
|
+
`\u68C0\u6D4B\u5230\u9879\u76EE\u7C7B\u578B: ${template || (isCustom ? "\u81EA\u5B9A\u4E49 (Custom)" : "\u672A\u77E5")}`
|
|
1825
|
+
);
|
|
1826
|
+
if (pageConfig) {
|
|
1827
|
+
logger2.info("\u4F7F\u7528\u9879\u76EE\u914D\u7F6E\u6587\u4EF6 (.flu-cli.json)");
|
|
1828
|
+
}
|
|
1829
|
+
const { checkSnippetsVersion: checkSnippetsVersion2 } = await import("./upgrade_snippets-XFR7Q444.js");
|
|
1830
|
+
const snippetsStatus = checkSnippetsVersion2(outputDir);
|
|
1831
|
+
if (snippetsStatus === "outdated") {
|
|
1832
|
+
logger2.warn("\u26A0\uFE0F \u68C0\u6D4B\u5230\u9879\u76EE\u4E2D\u7684 snippets \u6587\u4EF6\u5DF2\u8FC7\u65F6");
|
|
1833
|
+
logger2.info("\u{1F4A1} \u5EFA\u8BAE\u8FD0\u884C: flu-cli upgrade snippets");
|
|
1834
|
+
}
|
|
1835
|
+
const namePascal = toPascalCase(name);
|
|
1836
|
+
const nameSnake = toSnakeCase(name);
|
|
1837
|
+
const nameTitle = toTitleCase(name);
|
|
1838
|
+
let moduleName = feature || name;
|
|
1839
|
+
let pagesDir;
|
|
1840
|
+
if (pageConfig?.path) {
|
|
1841
|
+
pagesDir = join9(
|
|
1842
|
+
outputDir,
|
|
1843
|
+
pageConfig.path.replace("{feature}", moduleName)
|
|
1844
|
+
);
|
|
1845
|
+
} else {
|
|
1846
|
+
pagesDir = getPagePath(outputDir, moduleName);
|
|
1847
|
+
}
|
|
1848
|
+
const vmDir = getViewModelPath(outputDir, moduleName);
|
|
1849
|
+
const pageFileName = pageConfig?.fileName ? pageConfig.fileName.replace("{name}", nameSnake) : `${nameSnake}${pageConfig?.fileSuffix ?? "_page"}.dart`;
|
|
1850
|
+
const vmFileName = projectConfig?.generators?.viewModel?.fileName ? projectConfig.generators.viewModel.fileName.replace(
|
|
1851
|
+
"{name}",
|
|
1852
|
+
nameSnake
|
|
1853
|
+
) : `${nameSnake}${projectConfig?.generators?.viewModel?.fileSuffix ?? "_viewmodel"}.dart`;
|
|
1854
|
+
const pageFilePath = join9(pagesDir, pageFileName);
|
|
1855
|
+
const vmFilePath = join9(vmDir, vmFileName);
|
|
1856
|
+
if (!existsSync7(pagesDir)) {
|
|
1857
|
+
mkdirSync3(pagesDir, { recursive: true });
|
|
1858
|
+
}
|
|
1859
|
+
if (withViewModel && !existsSync7(vmDir)) {
|
|
1860
|
+
mkdirSync3(vmDir, { recursive: true });
|
|
1861
|
+
}
|
|
1862
|
+
const vmImportPath = calculateRelativeImport2(pageFilePath, vmFilePath);
|
|
1863
|
+
const variables = {
|
|
1864
|
+
Name: namePascal,
|
|
1865
|
+
name: nameSnake.replace(/_([a-z])/g, (_, c) => c.toUpperCase()),
|
|
1866
|
+
snake_name: nameSnake,
|
|
1867
|
+
title: nameTitle,
|
|
1868
|
+
vm_import: withViewModel ? vmImportPath : "",
|
|
1869
|
+
ModelName: namePascal + "Model"
|
|
1870
|
+
// 默认 Model 名称
|
|
1871
|
+
};
|
|
1872
|
+
let content = null;
|
|
1873
|
+
let key = "";
|
|
1874
|
+
if (pageConfig) {
|
|
1875
|
+
if (!pageConfig.withBasePage && !pageConfig.withViewModel) {
|
|
1876
|
+
if (pageConfig.defaultType === "stateless") {
|
|
1877
|
+
content = TemplateGenerator.generateSimpleStatelessPage(
|
|
1878
|
+
namePascal,
|
|
1879
|
+
nameTitle
|
|
1880
|
+
);
|
|
1881
|
+
} else {
|
|
1882
|
+
content = TemplateGenerator.generateSimpleStatefulPage(
|
|
1883
|
+
namePascal,
|
|
1884
|
+
nameTitle
|
|
1885
|
+
);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
if (!content && !pageConfig && !withViewModel) {
|
|
1890
|
+
if (stateless) {
|
|
1891
|
+
content = TemplateGenerator.generateSimpleStatelessPage(
|
|
1892
|
+
namePascal,
|
|
1893
|
+
nameTitle
|
|
1894
|
+
);
|
|
1895
|
+
logger2.info("\u751F\u6210\u7C7B\u578B: Simple Stateless Page (\u65E0\u914D\u7F6E\u6587\u4EF6)");
|
|
1896
|
+
} else {
|
|
1897
|
+
content = TemplateGenerator.generateSimpleStatefulPage(
|
|
1898
|
+
namePascal,
|
|
1899
|
+
nameTitle
|
|
1900
|
+
);
|
|
1901
|
+
logger2.info("\u751F\u6210\u7C7B\u578B: Simple Stateful Page (\u65E0\u914D\u7F6E\u6587\u4EF6)");
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
if (!content) {
|
|
1905
|
+
const { StateManagerAdapterFactory: StateManagerAdapterFactory2 } = await import("./factory-6DDXZYQP.js");
|
|
1906
|
+
const adapter = StateManagerAdapterFactory2.getAdapter(stateManager);
|
|
1907
|
+
const coreImportPath = getPackageImportPath(outputDir, "core/index.dart") || calculateRelativeImport2(pageFilePath, join9(outputDir, "lib", "core", "index.dart"));
|
|
1908
|
+
logger2.info(`[DEBUG] Core import path: ${coreImportPath}`);
|
|
1909
|
+
const variables2 = {
|
|
1910
|
+
Name: namePascal,
|
|
1911
|
+
name: nameSnake.replace(/_([a-z])/g, (_, c) => c.toUpperCase()),
|
|
1912
|
+
snake_name: nameSnake,
|
|
1913
|
+
title: nameTitle,
|
|
1914
|
+
vm_import: withViewModel ? vmImportPath : "",
|
|
1915
|
+
relative_core_path: coreImportPath,
|
|
1916
|
+
base_page: adapter.getBasePageParent?.(isListPage) || "BasePage",
|
|
1917
|
+
base_viewmodel: adapter.getBaseViewModelParent?.(isListPage) || "BaseViewModel"
|
|
1918
|
+
};
|
|
1919
|
+
if (stateManager === "riverpod") {
|
|
1920
|
+
key = "flu.riverpodPage";
|
|
1921
|
+
content = getSnippetContent(outputDir, key, variables2);
|
|
1922
|
+
} else if (isListPage) {
|
|
1923
|
+
key = "flu.listPage";
|
|
1924
|
+
} else if (stateless) {
|
|
1925
|
+
key = "flu.lessPage";
|
|
1926
|
+
} else {
|
|
1927
|
+
key = "flu.stPage";
|
|
1928
|
+
}
|
|
1929
|
+
if (!content) {
|
|
1930
|
+
content = getSnippetContent(outputDir, key, variables2);
|
|
1931
|
+
}
|
|
1932
|
+
if (!content) {
|
|
1933
|
+
const imports = adapter.getImports?.(coreImportPath) || [`import '${coreImportPath}';`];
|
|
1934
|
+
const baseClass = adapter.getBasePageParent?.(isListPage) || "BasePage";
|
|
1935
|
+
if (stateManager === "riverpod") {
|
|
1936
|
+
const camel = nameSnake.replace(/_([a-z])/g, (_, $1) => $1.toUpperCase());
|
|
1937
|
+
content = `${imports.join("\n")}
|
|
1938
|
+
import '${vmImportPath}';
|
|
1939
|
+
|
|
1940
|
+
class ${namePascal}Page extends ${baseClass}<${namePascal}State, ${namePascal}ViewModel> {
|
|
1941
|
+
const ${namePascal}Page({super.key});
|
|
1942
|
+
@override
|
|
1943
|
+
String get title => '${nameTitle}';
|
|
1944
|
+
@override
|
|
1945
|
+
ProviderListenable<${namePascal}State> get provider => ${camel}Provider;
|
|
1946
|
+
|
|
1947
|
+
@override
|
|
1948
|
+
Widget buildContent(BuildContext context, ${namePascal}State state, ${namePascal}ViewModel vm) {
|
|
1949
|
+
return Center(child: Text('${namePascal}Page'));
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
`;
|
|
1953
|
+
} else if (withViewModel && !stateless) {
|
|
1954
|
+
content = TemplateGenerator.generateStatefulPageWithBase(namePascal, nameTitle, {
|
|
1955
|
+
vmImportPath,
|
|
1956
|
+
coreImportPath,
|
|
1957
|
+
baseClass,
|
|
1958
|
+
extraImports: imports
|
|
1959
|
+
});
|
|
1960
|
+
} else if (stateless) {
|
|
1961
|
+
content = TemplateGenerator.generateSimpleStatelessPage(namePascal, nameTitle);
|
|
1962
|
+
} else {
|
|
1963
|
+
content = TemplateGenerator.generateSimpleStatefulPage(namePascal, nameTitle);
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
if (existsSync7(pageFilePath)) {
|
|
1968
|
+
logger2.error(`\u6587\u4EF6\u5DF2\u5B58\u5728: ${pageFilePath}`);
|
|
1969
|
+
return false;
|
|
1970
|
+
}
|
|
1971
|
+
writeFileSync4(pageFilePath, content, "utf8");
|
|
1972
|
+
logger2.success(`\u9875\u9762\u521B\u5EFA\u6210\u529F: ${pageFilePath}`);
|
|
1973
|
+
updateIndexFile(pagesDir, pageFileName);
|
|
1974
|
+
if (isListPage) {
|
|
1975
|
+
logger2.info("\u5217\u8868\u9875\u9700\u8981 Model\uFF0C\u6B63\u5728\u81EA\u52A8\u751F\u6210...");
|
|
1976
|
+
const modelFeature = template && (template.includes("modular") || template.includes("clean")) ? moduleName : null;
|
|
1977
|
+
generateModel(name, { feature: modelFeature, outputDir }, logger2);
|
|
1978
|
+
}
|
|
1979
|
+
if (withViewModel) {
|
|
1980
|
+
const vmFeature = template && (template.includes("modular") || template.includes("clean")) ? moduleName : null;
|
|
1981
|
+
generateViewModel(
|
|
1982
|
+
name,
|
|
1983
|
+
{ feature: vmFeature, outputDir, template, isListPage },
|
|
1984
|
+
logger2
|
|
1985
|
+
);
|
|
1986
|
+
}
|
|
1987
|
+
return true;
|
|
1988
|
+
} catch (error) {
|
|
1989
|
+
logger2.error(`\u751F\u6210\u9875\u9762\u5931\u8D25: ${error.message}`);
|
|
1990
|
+
return false;
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
function calculateRelativeImport2(fromFile, toFile) {
|
|
1994
|
+
const fromDir = dirname3(fromFile);
|
|
1995
|
+
const relPath = relative2(fromDir, toFile);
|
|
1996
|
+
return relPath.replace(/\\/g, "/");
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
// src/generators/widget_generator.ts
|
|
2000
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
2001
|
+
import { join as join10 } from "path";
|
|
2002
|
+
async function generateWidget(name, options = {}, logger2 = new ConsoleLogger()) {
|
|
2003
|
+
try {
|
|
2004
|
+
const {
|
|
2005
|
+
feature = null,
|
|
2006
|
+
stateful = false,
|
|
2007
|
+
outputDir = process.cwd()
|
|
2008
|
+
} = options;
|
|
2009
|
+
const projectConfig = ProjectConfigManager.loadConfig(outputDir);
|
|
2010
|
+
const widgetConfig = projectConfig?.generators?.widget;
|
|
2011
|
+
const template = detectProjectTemplate(outputDir);
|
|
2012
|
+
const isCustom = isCustomTemplateProject(outputDir);
|
|
2013
|
+
const namePascal = toPascalCase(name);
|
|
2014
|
+
const nameSnake = toSnakeCase(name);
|
|
2015
|
+
let widgetsDir;
|
|
2016
|
+
if (widgetConfig?.path) {
|
|
2017
|
+
widgetsDir = join10(outputDir, widgetConfig.path.replace("{feature}", feature || name));
|
|
2018
|
+
} else {
|
|
2019
|
+
widgetsDir = getWidgetPath(outputDir, feature);
|
|
2020
|
+
}
|
|
2021
|
+
if (!existsSync8(widgetsDir)) {
|
|
2022
|
+
mkdirSync4(widgetsDir, { recursive: true });
|
|
2023
|
+
}
|
|
2024
|
+
let content = null;
|
|
2025
|
+
const key = stateful ? "flu.stWidget" : "flu.lessWidget";
|
|
2026
|
+
if (widgetConfig) {
|
|
2027
|
+
content = getSimpleWidget(namePascal, stateful);
|
|
2028
|
+
}
|
|
2029
|
+
if (!content && !widgetConfig && isCustom) {
|
|
2030
|
+
content = getSimpleWidget(namePascal, stateful);
|
|
2031
|
+
}
|
|
2032
|
+
if (!content) {
|
|
2033
|
+
content = getSnippetContent(outputDir, key, { Name: namePascal });
|
|
2034
|
+
if (!content) {
|
|
2035
|
+
content = generateWidgetContent(namePascal, stateful);
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
const fileName = widgetConfig?.fileName ? widgetConfig.fileName.replace("{name}", nameSnake) : `${nameSnake}${widgetConfig?.fileSuffix ?? "_widget"}.dart`;
|
|
2039
|
+
const filePath = join10(widgetsDir, fileName);
|
|
2040
|
+
if (existsSync8(filePath)) {
|
|
2041
|
+
logger2.error(`\u6587\u4EF6\u5DF2\u5B58\u5728: ${filePath}`);
|
|
2042
|
+
return false;
|
|
2043
|
+
}
|
|
2044
|
+
writeFileSync5(filePath, content, "utf8");
|
|
2045
|
+
logger2.success(`Widget \u521B\u5EFA\u6210\u529F: ${filePath}`);
|
|
2046
|
+
updateIndexFile(widgetsDir, fileName);
|
|
2047
|
+
return true;
|
|
2048
|
+
} catch (error) {
|
|
2049
|
+
logger2.error(`\u751F\u6210 Widget \u5931\u8D25: ${error.message}`);
|
|
2050
|
+
return false;
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
function generateWidgetContent(namePascal, stateful) {
|
|
2054
|
+
if (stateful) {
|
|
2055
|
+
return `import 'package:flutter/material.dart';
|
|
2056
|
+
|
|
2057
|
+
class ${namePascal}Widget extends StatefulWidget {
|
|
2058
|
+
const ${namePascal}Widget({super.key});
|
|
2059
|
+
|
|
2060
|
+
@override
|
|
2061
|
+
State<${namePascal}Widget> createState() => _${namePascal}WidgetState();
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
class _${namePascal}WidgetState extends State<${namePascal}Widget> {
|
|
2065
|
+
@override
|
|
2066
|
+
Widget build(BuildContext context) {
|
|
2067
|
+
return Container(
|
|
2068
|
+
child: const Text('${namePascal}Widget'),
|
|
2069
|
+
);
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
`;
|
|
2073
|
+
} else {
|
|
2074
|
+
return `import 'package:flutter/material.dart';
|
|
2075
|
+
|
|
2076
|
+
class ${namePascal}Widget extends StatelessWidget {
|
|
2077
|
+
const ${namePascal}Widget({super.key});
|
|
2078
|
+
|
|
2079
|
+
@override
|
|
2080
|
+
Widget build(BuildContext context) {
|
|
2081
|
+
return Container(
|
|
2082
|
+
child: const Text('${namePascal}Widget'),
|
|
2083
|
+
);
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
`;
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
// src/generators/component_generator.ts
|
|
2091
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
2092
|
+
import { join as join11 } from "path";
|
|
2093
|
+
async function generateComponent(name, options = {}, logger2 = new ConsoleLogger()) {
|
|
2094
|
+
try {
|
|
2095
|
+
const {
|
|
2096
|
+
feature = null,
|
|
2097
|
+
stateful = false,
|
|
2098
|
+
outputDir = process.cwd()
|
|
2099
|
+
} = options;
|
|
2100
|
+
const namePascal = toPascalCase(name);
|
|
2101
|
+
const nameSnake = toSnakeCase(name);
|
|
2102
|
+
const projectConfig = ProjectConfigManager.loadConfig(outputDir);
|
|
2103
|
+
const componentConfig = projectConfig?.generators?.component;
|
|
2104
|
+
let componentsDir;
|
|
2105
|
+
if (componentConfig?.path) {
|
|
2106
|
+
componentsDir = join11(outputDir, componentConfig.path.replace("{feature}", feature || name));
|
|
2107
|
+
} else if (feature) {
|
|
2108
|
+
componentsDir = join11(outputDir, "lib", "features", feature, "components");
|
|
2109
|
+
} else {
|
|
2110
|
+
componentsDir = join11(outputDir, "lib", "components");
|
|
2111
|
+
}
|
|
2112
|
+
if (!existsSync9(componentsDir)) {
|
|
2113
|
+
mkdirSync5(componentsDir, { recursive: true });
|
|
2114
|
+
}
|
|
2115
|
+
const snippetKey = stateful ? "flu.stComponent" : "flu.component";
|
|
2116
|
+
const content = getSnippetContent(outputDir, snippetKey, { Name: namePascal }) || generateComponentContent(namePascal, stateful);
|
|
2117
|
+
logger2.info(`\u751F\u6210\u7C7B\u578B: ${stateful ? "Stateful" : "Stateless"} Component`);
|
|
2118
|
+
const fileName = componentConfig?.fileName ? componentConfig.fileName.replace("{name}", nameSnake) : `${nameSnake}${componentConfig?.fileSuffix ?? "_component"}.dart`;
|
|
2119
|
+
const filePath = join11(componentsDir, fileName);
|
|
2120
|
+
if (existsSync9(filePath)) {
|
|
2121
|
+
logger2.error(`\u6587\u4EF6\u5DF2\u5B58\u5728: ${filePath}`);
|
|
2122
|
+
return false;
|
|
2123
|
+
}
|
|
2124
|
+
writeFileSync6(filePath, content, "utf8");
|
|
2125
|
+
logger2.success(`Component \u521B\u5EFA\u6210\u529F: ${filePath}`);
|
|
2126
|
+
updateIndexFile(componentsDir, fileName);
|
|
2127
|
+
return true;
|
|
2128
|
+
} catch (error) {
|
|
2129
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2130
|
+
logger2.error(`\u751F\u6210 Component \u5931\u8D25: ${errorMessage}`);
|
|
2131
|
+
return false;
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
function generateComponentContent(namePascal, stateful = false) {
|
|
2135
|
+
if (stateful) {
|
|
2136
|
+
return `import 'package:flutter/material.dart';
|
|
2137
|
+
|
|
2138
|
+
class ${namePascal}Component extends StatefulWidget {
|
|
2139
|
+
const ${namePascal}Component({super.key});
|
|
2140
|
+
|
|
2141
|
+
@override
|
|
2142
|
+
State<${namePascal}Component> createState() => _${namePascal}ComponentState();
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
class _${namePascal}ComponentState extends State<${namePascal}Component> {
|
|
2146
|
+
@override
|
|
2147
|
+
Widget build(BuildContext context) {
|
|
2148
|
+
return Container(
|
|
2149
|
+
padding: const EdgeInsets.all(16),
|
|
2150
|
+
child: Column(
|
|
2151
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
2152
|
+
children: [
|
|
2153
|
+
Text(
|
|
2154
|
+
'${namePascal}Component',
|
|
2155
|
+
style: Theme.of(context).textTheme.titleLarge,
|
|
2156
|
+
),
|
|
2157
|
+
const SizedBox(height: 8),
|
|
2158
|
+
const Text('Component content goes here'),
|
|
2159
|
+
],
|
|
2160
|
+
),
|
|
2161
|
+
);
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
`;
|
|
2165
|
+
}
|
|
2166
|
+
return `import 'package:flutter/material.dart';
|
|
2167
|
+
|
|
2168
|
+
class ${namePascal}Component extends StatelessWidget {
|
|
2169
|
+
const ${namePascal}Component({super.key});
|
|
2170
|
+
|
|
2171
|
+
@override
|
|
2172
|
+
Widget build(BuildContext context) {
|
|
2173
|
+
return Container(
|
|
2174
|
+
padding: const EdgeInsets.all(16),
|
|
2175
|
+
child: Column(
|
|
2176
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
2177
|
+
children: [
|
|
2178
|
+
Text(
|
|
2179
|
+
'${namePascal}Component',
|
|
2180
|
+
style: Theme.of(context).textTheme.titleLarge,
|
|
2181
|
+
),
|
|
2182
|
+
const SizedBox(height: 8),
|
|
2183
|
+
const Text('Component content goes here'),
|
|
2184
|
+
],
|
|
2185
|
+
),
|
|
2186
|
+
);
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
`;
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
// src/generators/service_generator.ts
|
|
2193
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync6, writeFileSync as writeFileSync7 } from "fs";
|
|
2194
|
+
import { join as join12, relative as relative3 } from "path";
|
|
2195
|
+
async function generateService(name, options = {}, customLogger) {
|
|
2196
|
+
try {
|
|
2197
|
+
const { feature = null, outputDir = process.cwd() } = options;
|
|
2198
|
+
const namePascal = toPascalCase(name);
|
|
2199
|
+
const nameSnake = toSnakeCase(name);
|
|
2200
|
+
const projectConfig = ProjectConfigManager.loadConfig(outputDir);
|
|
2201
|
+
const serviceConfig = projectConfig?.generators?.service;
|
|
2202
|
+
let servicesDir;
|
|
2203
|
+
if (serviceConfig?.path) {
|
|
2204
|
+
servicesDir = join12(outputDir, serviceConfig.path.replace("{feature}", feature || name));
|
|
2205
|
+
} else {
|
|
2206
|
+
servicesDir = getServicePath(outputDir, feature);
|
|
2207
|
+
}
|
|
2208
|
+
if (!existsSync10(servicesDir)) {
|
|
2209
|
+
mkdirSync6(servicesDir, { recursive: true });
|
|
2210
|
+
}
|
|
2211
|
+
const templateType = selectTemplate(outputDir, serviceConfig);
|
|
2212
|
+
let content = null;
|
|
2213
|
+
const snippetKey = serviceConfig?.snippetKey || "flu.service";
|
|
2214
|
+
content = getSnippetContent(outputDir, snippetKey, {
|
|
2215
|
+
Name: namePascal,
|
|
2216
|
+
snake_name: nameSnake,
|
|
2217
|
+
relative_core_path: calculateCoreImport(servicesDir, outputDir)
|
|
2218
|
+
});
|
|
2219
|
+
if (!content) {
|
|
2220
|
+
if (templateType === "network") {
|
|
2221
|
+
content = generateNetworkService(namePascal, nameSnake, servicesDir, outputDir);
|
|
2222
|
+
} else {
|
|
2223
|
+
content = generateSimpleService(namePascal);
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
const fileName = serviceConfig?.fileName ? serviceConfig.fileName.replace("{name}", nameSnake) : `${nameSnake}_service.dart`;
|
|
2227
|
+
const filePath = join12(servicesDir, fileName);
|
|
2228
|
+
if (existsSync10(filePath)) {
|
|
2229
|
+
logger.error(`\u6587\u4EF6\u5DF2\u5B58\u5728: ${filePath}`);
|
|
2230
|
+
return false;
|
|
2231
|
+
}
|
|
2232
|
+
writeFileSync7(filePath, content, "utf8");
|
|
2233
|
+
logger.success(`Service \u521B\u5EFA\u6210\u529F: ${filePath}`);
|
|
2234
|
+
updateIndexFile(servicesDir, fileName);
|
|
2235
|
+
return true;
|
|
2236
|
+
} catch (error) {
|
|
2237
|
+
logger.error(
|
|
2238
|
+
`\u751F\u6210 Service \u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`
|
|
2239
|
+
);
|
|
2240
|
+
return false;
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
function selectTemplate(outputDir, config) {
|
|
2244
|
+
if (config?.template) {
|
|
2245
|
+
return config.template === "network" ? "network" : "simple";
|
|
2246
|
+
}
|
|
2247
|
+
if (hasNetworkLayer(outputDir)) {
|
|
2248
|
+
return "network";
|
|
2249
|
+
}
|
|
2250
|
+
return "simple";
|
|
2251
|
+
}
|
|
2252
|
+
function hasNetworkLayer(outputDir) {
|
|
2253
|
+
const appHttpPath = join12(outputDir, "lib/core/network/app_http.dart");
|
|
2254
|
+
return existsSync10(appHttpPath);
|
|
2255
|
+
}
|
|
2256
|
+
function calculateCoreImport(fromDir, projectRoot) {
|
|
2257
|
+
const coreIndexPath = join12(projectRoot, "lib/core/index.dart");
|
|
2258
|
+
const relPath = relative3(fromDir, coreIndexPath);
|
|
2259
|
+
return relPath.replace(/\\/g, "/");
|
|
2260
|
+
}
|
|
2261
|
+
function generateNetworkService(namePascal, nameSnake, servicesDir, projectRoot) {
|
|
2262
|
+
const coreImport = calculateCoreImport(servicesDir, projectRoot);
|
|
2263
|
+
return `import '${coreImport}';
|
|
2264
|
+
|
|
2265
|
+
class ${namePascal}Service {
|
|
2266
|
+
final AppHttp _http;
|
|
2267
|
+
|
|
2268
|
+
${namePascal}Service({AppHttp? http}) : _http = http ?? AppHttp();
|
|
2269
|
+
|
|
2270
|
+
/// \u83B7\u53D6\u5217\u8868\u6570\u636E
|
|
2271
|
+
Future<List<dynamic>> fetchList({
|
|
2272
|
+
int page = 1,
|
|
2273
|
+
int pageSize = 10,
|
|
2274
|
+
}) async {
|
|
2275
|
+
if (AppConfig.I.useMockData) {
|
|
2276
|
+
return _loadMockData(page, pageSize);
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
final response = await _http.get(
|
|
2280
|
+
'/${nameSnake}/list',
|
|
2281
|
+
queryParameters: {'page': page, 'pageSize': pageSize},
|
|
2282
|
+
);
|
|
2283
|
+
|
|
2284
|
+
// \u964D\u7EA7\u5904\u7406\uFF1A\u8BF7\u6C42\u5931\u8D25\u65F6\u8FD4\u56DE Mock \u6570\u636E
|
|
2285
|
+
if (response.isSuccess && response.data is List) {
|
|
2286
|
+
return response.data as List;
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
return _loadMockData(page, pageSize);
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
/// \u6839\u636E ID \u83B7\u53D6\u8BE6\u60C5
|
|
2293
|
+
Future<dynamic> fetchById(String id) async {
|
|
2294
|
+
if (AppConfig.I.useMockData) {
|
|
2295
|
+
return null; // \u8FD4\u56DE Mock \u6570\u636E
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
final response = await _http.get('/${nameSnake}/$id');
|
|
2299
|
+
return response.isSuccess ? response.data : null;
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
/// Mock \u6570\u636E\u52A0\u8F7D
|
|
2303
|
+
List<dynamic> _loadMockData(int page, int pageSize) {
|
|
2304
|
+
// \u6A21\u62DF\u6570\u636E\u52A0\u8F7D\u903B\u8F91
|
|
2305
|
+
// \u793A\u4F8B: \u8FD4\u56DE\u6D4B\u8BD5\u6570\u636E\u6216\u4ECE\u672C\u5730\u5B58\u50A8\u8BFB\u53D6
|
|
2306
|
+
return [];
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
`;
|
|
2310
|
+
}
|
|
2311
|
+
function generateSimpleService(namePascal) {
|
|
2312
|
+
return `class ${namePascal}Service {
|
|
2313
|
+
/// \u83B7\u53D6\u6570\u636E\u5217\u8868
|
|
2314
|
+
Future<List<Map<String, dynamic>>> fetchList() async {
|
|
2315
|
+
// \u5B9E\u73B0\u4E1A\u52A1\u903B\u8F91
|
|
2316
|
+
// \u4F8B\u5982: \u4ECE\u672C\u5730\u5B58\u50A8\u8BFB\u53D6\u3001\u8BA1\u7B97\u7B49
|
|
2317
|
+
return [];
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
/// \u6839\u636E ID \u83B7\u53D6\u6570\u636E
|
|
2321
|
+
Future<Map<String, dynamic>?> fetchById(String id) async {
|
|
2322
|
+
// \u5B9E\u73B0\u4E1A\u52A1\u903B\u8F91
|
|
2323
|
+
return null;
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
/// \u4FDD\u5B58\u6216\u66F4\u65B0\u6570\u636E
|
|
2327
|
+
Future<bool> save(Map<String, dynamic> data) async {
|
|
2328
|
+
// \u5B9E\u73B0\u4E1A\u52A1\u903B\u8F91
|
|
2329
|
+
return true;
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
`;
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
// src/generators/module_generator.ts
|
|
2336
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
|
|
2337
|
+
import { join as join13 } from "path";
|
|
2338
|
+
async function generateModule(name, options = {}, logger2 = new ConsoleLogger()) {
|
|
2339
|
+
try {
|
|
2340
|
+
const {
|
|
2341
|
+
outputDir = process.cwd()
|
|
2342
|
+
} = options;
|
|
2343
|
+
const nameSnake = toSnakeCase(name);
|
|
2344
|
+
const projectConfig = ProjectConfigManager.loadConfig(outputDir);
|
|
2345
|
+
const moduleConfig = projectConfig?.generators?.module;
|
|
2346
|
+
const moduleDir = moduleConfig?.path ? join13(outputDir, moduleConfig.path.replace("{feature}", nameSnake)) : join13(outputDir, "lib", "features", nameSnake);
|
|
2347
|
+
if (existsSync11(moduleDir)) {
|
|
2348
|
+
logger2.error(`\u6A21\u5757\u5DF2\u5B58\u5728: ${moduleDir}`);
|
|
2349
|
+
return false;
|
|
2350
|
+
}
|
|
2351
|
+
const directories = [
|
|
2352
|
+
"pages",
|
|
2353
|
+
"viewmodels",
|
|
2354
|
+
"widgets",
|
|
2355
|
+
"services",
|
|
2356
|
+
"models"
|
|
2357
|
+
];
|
|
2358
|
+
logger2.info(`\u521B\u5EFA\u6A21\u5757: ${nameSnake}`);
|
|
2359
|
+
for (const dir of directories) {
|
|
2360
|
+
const dirPath = join13(moduleDir, dir);
|
|
2361
|
+
mkdirSync7(dirPath, { recursive: true });
|
|
2362
|
+
const indexPath = join13(dirPath, "index.dart");
|
|
2363
|
+
const indexContent = generateIndexContent(dir, nameSnake);
|
|
2364
|
+
writeFileSync8(indexPath, indexContent, "utf8");
|
|
2365
|
+
logger2.success(`\u521B\u5EFA\u76EE\u5F55: ${dir}/`);
|
|
2366
|
+
}
|
|
2367
|
+
const moduleIndexPath = join13(moduleDir, "index.dart");
|
|
2368
|
+
const moduleIndexContent = generateModuleIndexContent(nameSnake);
|
|
2369
|
+
writeFileSync8(moduleIndexPath, moduleIndexContent, "utf8");
|
|
2370
|
+
logger2.newLine();
|
|
2371
|
+
logger2.success(`\u2705 \u6A21\u5757\u521B\u5EFA\u6210\u529F: features/${nameSnake}/`);
|
|
2372
|
+
logger2.newLine();
|
|
2373
|
+
logger2.info("\u6A21\u5757\u7ED3\u6784:");
|
|
2374
|
+
logger2.info(` features/${nameSnake}/`);
|
|
2375
|
+
logger2.info(` \u251C\u2500\u2500 pages/`);
|
|
2376
|
+
logger2.info(` \u2502 \u2514\u2500\u2500 index.dart`);
|
|
2377
|
+
logger2.info(` \u251C\u2500\u2500 viewmodels/`);
|
|
2378
|
+
logger2.info(` \u2502 \u2514\u2500\u2500 index.dart`);
|
|
2379
|
+
logger2.info(` \u251C\u2500\u2500 widgets/`);
|
|
2380
|
+
logger2.info(` \u2502 \u2514\u2500\u2500 index.dart`);
|
|
2381
|
+
logger2.info(` \u251C\u2500\u2500 services/`);
|
|
2382
|
+
logger2.info(` \u2502 \u2514\u2500\u2500 index.dart`);
|
|
2383
|
+
logger2.info(` \u251C\u2500\u2500 models/`);
|
|
2384
|
+
logger2.info(` \u2502 \u2514\u2500\u2500 index.dart`);
|
|
2385
|
+
logger2.info(` \u2514\u2500\u2500 index.dart`);
|
|
2386
|
+
logger2.newLine();
|
|
2387
|
+
logger2.info("\u4E0B\u4E00\u6B65:");
|
|
2388
|
+
logger2.info(` 1. \u4F7F\u7528 "flu-cli add page <name> -f ${nameSnake}" \u6DFB\u52A0\u9875\u9762`);
|
|
2389
|
+
logger2.info(` 2. \u4F7F\u7528 "flu-cli add service <name> -f ${nameSnake}" \u6DFB\u52A0\u670D\u52A1`);
|
|
2390
|
+
logger2.info(` 3. \u4F7F\u7528 "flu-cli add model <name> -f ${nameSnake}" \u6DFB\u52A0\u6A21\u578B`);
|
|
2391
|
+
return true;
|
|
2392
|
+
} catch (error) {
|
|
2393
|
+
logger2.error(`\u6A21\u5757\u521B\u5EFA\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`);
|
|
2394
|
+
return false;
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
function generateIndexContent(dirName, moduleName) {
|
|
2398
|
+
const comments = {
|
|
2399
|
+
pages: "// \u5BFC\u51FA\u6240\u6709\u9875\u9762",
|
|
2400
|
+
viewmodels: "// \u5BFC\u51FA\u6240\u6709 ViewModel",
|
|
2401
|
+
widgets: "// \u5BFC\u51FA\u6240\u6709 Widget",
|
|
2402
|
+
services: "// \u5BFC\u51FA\u6240\u6709 Service",
|
|
2403
|
+
models: "// \u5BFC\u51FA\u6240\u6709 Model"
|
|
2404
|
+
};
|
|
2405
|
+
const examples = {
|
|
2406
|
+
pages: `// export 'home_page.dart';`,
|
|
2407
|
+
viewmodels: `// export 'home_viewmodel.dart';`,
|
|
2408
|
+
widgets: `// export 'custom_widget.dart';`,
|
|
2409
|
+
services: `// export 'api_service.dart';`,
|
|
2410
|
+
models: `// export 'user_model.dart';`
|
|
2411
|
+
};
|
|
2412
|
+
return `${comments[dirName] || "// \u5BFC\u51FA\u6587\u4EF6"}
|
|
2413
|
+
|
|
2414
|
+
${examples[dirName] || "// export 'example.dart';"}
|
|
2415
|
+
`;
|
|
2416
|
+
}
|
|
2417
|
+
function generateModuleIndexContent(moduleName) {
|
|
2418
|
+
return `// ${moduleName} \u6A21\u5757\u5BFC\u51FA
|
|
2419
|
+
|
|
2420
|
+
// \u5BFC\u51FA\u9875\u9762
|
|
2421
|
+
export 'pages/index.dart';
|
|
2422
|
+
|
|
2423
|
+
// \u5BFC\u51FA ViewModel
|
|
2424
|
+
export 'viewmodels/index.dart';
|
|
2425
|
+
|
|
2426
|
+
// \u5BFC\u51FA Widget
|
|
2427
|
+
export 'widgets/index.dart';
|
|
2428
|
+
|
|
2429
|
+
// \u5BFC\u51FA Service
|
|
2430
|
+
export 'services/index.dart';
|
|
2431
|
+
|
|
2432
|
+
// \u5BFC\u51FA Model
|
|
2433
|
+
export 'models/index.dart';
|
|
2434
|
+
`;
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
// src/generators/project_generator.ts
|
|
2438
|
+
import { existsSync as existsSync13 } from "fs";
|
|
2439
|
+
import { join as join24 } from "path";
|
|
2440
|
+
|
|
2441
|
+
// src/utils/template_manager.ts
|
|
2442
|
+
import { simpleGit } from "simple-git";
|
|
2443
|
+
import { join as join15, resolve, isAbsolute } from "path";
|
|
2444
|
+
import fsx4 from "fs-extra";
|
|
2445
|
+
import { homedir as homedir2 } from "os";
|
|
2446
|
+
|
|
2447
|
+
// src/utils/config_manager.ts
|
|
2448
|
+
import { existsSync as existsSync12, readFileSync as readFileSync5, writeFileSync as writeFileSync9, mkdirSync as mkdirSync8 } from "fs";
|
|
2449
|
+
import { join as join14 } from "path";
|
|
2450
|
+
import { homedir } from "os";
|
|
2451
|
+
var ConfigManager = class _ConfigManager {
|
|
2452
|
+
static instance;
|
|
2453
|
+
configPath;
|
|
2454
|
+
config;
|
|
2455
|
+
constructor() {
|
|
2456
|
+
const configDir = join14(homedir(), ".flu-cli");
|
|
2457
|
+
if (!existsSync12(configDir)) {
|
|
2458
|
+
mkdirSync8(configDir, { recursive: true });
|
|
2459
|
+
}
|
|
2460
|
+
this.configPath = join14(configDir, "config.json");
|
|
2461
|
+
this.config = this.loadConfig();
|
|
2462
|
+
}
|
|
2463
|
+
static getInstance() {
|
|
2464
|
+
if (!_ConfigManager.instance) {
|
|
2465
|
+
_ConfigManager.instance = new _ConfigManager();
|
|
2466
|
+
}
|
|
2467
|
+
return _ConfigManager.instance;
|
|
2468
|
+
}
|
|
2469
|
+
loadConfig() {
|
|
2470
|
+
if (existsSync12(this.configPath)) {
|
|
2471
|
+
try {
|
|
2472
|
+
const content = readFileSync5(this.configPath, "utf8");
|
|
2473
|
+
const parsed = JSON.parse(content);
|
|
2474
|
+
return {
|
|
2475
|
+
templates: parsed.templates || [],
|
|
2476
|
+
authorName: parsed.authorName,
|
|
2477
|
+
defaultTemplate: parsed.defaultTemplate,
|
|
2478
|
+
locale: parsed.locale
|
|
2479
|
+
};
|
|
2480
|
+
} catch (e) {
|
|
2481
|
+
return { templates: [] };
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
return { templates: [] };
|
|
2485
|
+
}
|
|
2486
|
+
saveConfig() {
|
|
2487
|
+
writeFileSync9(this.configPath, JSON.stringify(this.config, null, 2), "utf8");
|
|
2488
|
+
}
|
|
2489
|
+
// --- 模板管理 ---
|
|
2490
|
+
getTemplates() {
|
|
2491
|
+
return this.config.templates || [];
|
|
2492
|
+
}
|
|
2493
|
+
addTemplate(template) {
|
|
2494
|
+
if (!this.config.templates) {
|
|
2495
|
+
this.config.templates = [];
|
|
2496
|
+
}
|
|
2497
|
+
const index = this.config.templates.findIndex((t2) => t2.id === template.id);
|
|
2498
|
+
if (index !== -1) {
|
|
2499
|
+
this.config.templates[index] = { ...this.config.templates[index], ...template, lastUsedAt: Date.now() };
|
|
2500
|
+
} else {
|
|
2501
|
+
this.config.templates.push({ ...template, lastUsedAt: Date.now() });
|
|
2502
|
+
}
|
|
2503
|
+
this.saveConfig();
|
|
2504
|
+
}
|
|
2505
|
+
removeTemplate(id) {
|
|
2506
|
+
if (!this.config.templates) return false;
|
|
2507
|
+
const initialLength = this.config.templates.length;
|
|
2508
|
+
this.config.templates = this.config.templates.filter((t2) => t2.id !== id);
|
|
2509
|
+
if (this.config.templates.length !== initialLength) {
|
|
2510
|
+
this.saveConfig();
|
|
2511
|
+
return true;
|
|
2512
|
+
}
|
|
2513
|
+
return false;
|
|
2514
|
+
}
|
|
2515
|
+
getTemplate(id) {
|
|
2516
|
+
return this.config.templates?.find((t2) => t2.id === id);
|
|
2517
|
+
}
|
|
2518
|
+
// --- 全局设置 ---
|
|
2519
|
+
getAuthorName() {
|
|
2520
|
+
return this.config.authorName || "Your Name";
|
|
2521
|
+
}
|
|
2522
|
+
setAuthorName(name) {
|
|
2523
|
+
this.config.authorName = name;
|
|
2524
|
+
this.saveConfig();
|
|
2525
|
+
}
|
|
2526
|
+
getDefaultTemplate() {
|
|
2527
|
+
return this.config.defaultTemplate || { type: "builtin", idOrName: "lite" };
|
|
2528
|
+
}
|
|
2529
|
+
setDefaultTemplate(type, idOrName) {
|
|
2530
|
+
this.config.defaultTemplate = { type, idOrName };
|
|
2531
|
+
this.saveConfig();
|
|
2532
|
+
}
|
|
2533
|
+
getLocale() {
|
|
2534
|
+
return this.config.locale || "";
|
|
2535
|
+
}
|
|
2536
|
+
setLocale(locale) {
|
|
2537
|
+
this.config.locale = locale;
|
|
2538
|
+
this.saveConfig();
|
|
2539
|
+
}
|
|
2540
|
+
};
|
|
2541
|
+
|
|
2542
|
+
// src/utils/template_manager.ts
|
|
2543
|
+
function loadEnvFile() {
|
|
2544
|
+
try {
|
|
2545
|
+
const possiblePaths = [
|
|
2546
|
+
resolve(process.cwd(), ".env"),
|
|
2547
|
+
resolve(process.cwd(), "..", ".env"),
|
|
2548
|
+
resolve(process.cwd(), "../..", ".env"),
|
|
2549
|
+
resolve(process.cwd(), "../../..", ".env")
|
|
2550
|
+
];
|
|
2551
|
+
for (const envPath of possiblePaths) {
|
|
2552
|
+
if (fsx4.existsSync(envPath)) {
|
|
2553
|
+
const envContent = fsx4.readFileSync(envPath, "utf8");
|
|
2554
|
+
const lines = envContent.split("\n");
|
|
2555
|
+
for (const line of lines) {
|
|
2556
|
+
const trimmed = line.trim();
|
|
2557
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2558
|
+
const [key, ...valueParts] = trimmed.split("=");
|
|
2559
|
+
const value = valueParts.join("=").trim();
|
|
2560
|
+
const cleanValue = value.replace(/^["']|["']$/g, "");
|
|
2561
|
+
process.env[key] = cleanValue;
|
|
2562
|
+
}
|
|
2563
|
+
break;
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
} catch (error) {
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
var BUILTIN_TEMPLATES = {
|
|
2570
|
+
lite: {
|
|
2571
|
+
name: "Lite",
|
|
2572
|
+
displayName: "Lite - \u7CBE\u7B80\u7248",
|
|
2573
|
+
description: "\u6700\u5C0F\u4F9D\u8D56\uFF0C\u5355\u6587\u4EF6\u7ED3\u6784\uFF0C\u9002\u5408\u5C0F\u578B\u9879\u76EE\u548C\u5FEB\u901F\u539F\u578B",
|
|
2574
|
+
repo: "https://gitee.com/flu-cli/template_lite.git",
|
|
2575
|
+
branch: "main",
|
|
2576
|
+
complexity: 1,
|
|
2577
|
+
teamSize: "1 \u4EBA",
|
|
2578
|
+
codeSize: "< 5k \u884C",
|
|
2579
|
+
features: ["\u2705 \u6700\u5C0F\u4F9D\u8D56", "\u2705 \u5355\u6587\u4EF6\u7ED3\u6784", "\u2705 Material 3 \u8BBE\u8BA1", "\u2705 \u5FEB\u901F\u542F\u52A8"],
|
|
2580
|
+
structure: `
|
|
2581
|
+
lib/
|
|
2582
|
+
\u251C\u2500\u2500 \u{1F4C4} main.dart # \u5E94\u7528\u5165\u53E3
|
|
2583
|
+
\u251C\u2500\u2500 \u{1F4C4} app.dart # \u5E94\u7528\u914D\u7F6E
|
|
2584
|
+
\u251C\u2500\u2500 \u{1F4C1} pages/ # \u9875\u9762
|
|
2585
|
+
\u251C\u2500\u2500 \u{1F4C1} viewmodels/ # \u89C6\u56FE\u6A21\u578B
|
|
2586
|
+
\u251C\u2500\u2500 \u{1F4C1} widgets/ # \u7B80\u5355\u7EC4\u4EF6
|
|
2587
|
+
\u251C\u2500\u2500 \u{1F4C1} components/ # \u590D\u5408\u7EC4\u4EF6
|
|
2588
|
+
\u251C\u2500\u2500 \u{1F4C1} services/ # \u4E1A\u52A1\u670D\u52A1
|
|
2589
|
+
\u251C\u2500\u2500 \u{1F4C1} models/ # \u6570\u636E\u6A21\u578B
|
|
2590
|
+
\u251C\u2500\u2500 \u{1F4C1} config/ # \u914D\u7F6E
|
|
2591
|
+
\u2514\u2500\u2500 \u{1F4C1} utils/ # \u5DE5\u5177\u7C7B
|
|
2592
|
+
`
|
|
2593
|
+
},
|
|
2594
|
+
modular: {
|
|
2595
|
+
name: "Modular",
|
|
2596
|
+
displayName: "Modular - \u6A21\u5757\u5316\u7248",
|
|
2597
|
+
description: "\u6A21\u5757\u5316\u7ED3\u6784\uFF0C\u529F\u80FD\u5206\u7EC4\uFF0C\u9002\u5408\u4E2D\u578B\u9879\u76EE",
|
|
2598
|
+
repo: "https://gitee.com/flu-cli/template_modular.git",
|
|
2599
|
+
branch: "main",
|
|
2600
|
+
complexity: 3,
|
|
2601
|
+
teamSize: "2-5 \u4EBA",
|
|
2602
|
+
codeSize: "5k-20k \u884C",
|
|
2603
|
+
features: [
|
|
2604
|
+
"\u2705 \u6A21\u5757\u5316\u7ED3\u6784",
|
|
2605
|
+
"\u2705 \u8DEF\u7531\u914D\u7F6E\u72EC\u7ACB",
|
|
2606
|
+
"\u2705 \u4E3B\u9898\u914D\u7F6E\u72EC\u7ACB",
|
|
2607
|
+
"\u2705 \u529F\u80FD\u5206\u7EC4"
|
|
2608
|
+
],
|
|
2609
|
+
structure: `
|
|
2610
|
+
lib/
|
|
2611
|
+
\u251C\u2500\u2500 \u{1F4C4} main.dart
|
|
2612
|
+
\u251C\u2500\u2500 \u{1F4C1} core/ # \u6838\u5FC3\u57FA\u7840\u8BBE\u65BD
|
|
2613
|
+
\u2502 \u251C\u2500\u2500 theme/ # \u4E3B\u9898\u7CFB\u7EDF
|
|
2614
|
+
\u2502 \u251C\u2500\u2500 router/ # \u8DEF\u7531\u7CFB\u7EDF
|
|
2615
|
+
\u2502 \u251C\u2500\u2500 constants/ # \u5E38\u91CF\u5B9A\u4E49
|
|
2616
|
+
\u2502 \u2514\u2500\u2500 base/ # \u57FA\u7840\u7C7B\uFF08BaseViewModel, BasePage\uFF09
|
|
2617
|
+
\u251C\u2500\u2500 \u{1F4C1} shared/ # \u8DE8\u6A21\u5757\u5171\u4EAB
|
|
2618
|
+
\u2502 \u251C\u2500\u2500 widgets/ # \u901A\u7528\u7B80\u5355\u7EC4\u4EF6
|
|
2619
|
+
\u2502 \u251C\u2500\u2500 components/ # \u901A\u7528\u590D\u5408\u7EC4\u4EF6
|
|
2620
|
+
\u2502 \u251C\u2500\u2500 models/ # \u901A\u7528\u6570\u636E\u6A21\u578B
|
|
2621
|
+
\u2502 \u2514\u2500\u2500 utils/ # \u901A\u7528\u5DE5\u5177\u7C7B
|
|
2622
|
+
\u2514\u2500\u2500 \u{1F4C1} features/ # \u4E1A\u52A1\u529F\u80FD\u6A21\u5757
|
|
2623
|
+
\u251C\u2500\u2500 hoem/ # \u7528\u6237\u6A21\u5757
|
|
2624
|
+
\u2502 \u251C\u2500\u2500 pages/
|
|
2625
|
+
\u2502 \u251C\u2500\u2500 viewmodels/
|
|
2626
|
+
\u2502 \u251C\u2500\u2500 widgets/
|
|
2627
|
+
\u2502 \u251C\u2500\u2500 services/
|
|
2628
|
+
\u2514\u2500\u2500 \u2514\u2500\u2500 models/
|
|
2629
|
+
`
|
|
2630
|
+
},
|
|
2631
|
+
clean: {
|
|
2632
|
+
name: "Clean",
|
|
2633
|
+
displayName: "Clean - \u5206\u5C42\u7248",
|
|
2634
|
+
description: "\u4E25\u683C\u5206\u5C42\u67B6\u6784\uFF0C\u6E05\u6670\u7684\u76EE\u5F55\u7ED3\u6784\uFF0C\u9002\u5408\u5927\u578B\u9879\u76EE",
|
|
2635
|
+
repo: "https://gitee.com/flu-cli/template_clean.git",
|
|
2636
|
+
branch: "main",
|
|
2637
|
+
complexity: 5,
|
|
2638
|
+
teamSize: "5+ \u4EBA",
|
|
2639
|
+
codeSize: "> 20k \u884C",
|
|
2640
|
+
features: [
|
|
2641
|
+
"\u2705 \u4E25\u683C\u5206\u5C42\u67B6\u6784",
|
|
2642
|
+
"\u2705 \u6E05\u6670\u7684\u76EE\u5F55\u7ED3\u6784",
|
|
2643
|
+
"\u2705 \u6613\u4E8E\u6269\u5C55\u548C\u7EF4\u62A4",
|
|
2644
|
+
"\u2705 \u9002\u5408\u56E2\u961F\u534F\u4F5C"
|
|
2645
|
+
],
|
|
2646
|
+
structure: `
|
|
2647
|
+
lib/
|
|
2648
|
+
\u251C\u2500\u2500 \u{1F4C4} main.dart # \u5E94\u7528\u5165\u53E3
|
|
2649
|
+
\u251C\u2500\u2500 \u{1F4C4} app.dart # \u5E94\u7528\u914D\u7F6E
|
|
2650
|
+
\u251C\u2500\u2500 \u{1F4C1} core/ # \u6838\u5FC3\u5C42(\u6700\u5185\u5C42)
|
|
2651
|
+
\u2502 \u251C\u2500\u2500 constants/ # \u5E38\u91CF\u5B9A\u4E49
|
|
2652
|
+
\u2502 \u251C\u2500\u2500 errors/ # \u9519\u8BEF\u548C\u5F02\u5E38
|
|
2653
|
+
\u2502 \u251C\u2500\u2500 usecases/ # UseCase \u57FA\u7C7B
|
|
2654
|
+
\u2502 \u251C\u2500\u2500 utils/ # \u5DE5\u5177\u7C7B
|
|
2655
|
+
\u2502 \u2514\u2500\u2500 network/ # \u7F51\u7EDC\u914D\u7F6E
|
|
2656
|
+
\u251C\u2500\u2500 \u{1F4C1} features/ # \u529F\u80FD\u6A21\u5757(\u6309\u4E1A\u52A1\u5212\u5206)
|
|
2657
|
+
\u2502 \u2514\u2500\u2500 home/ # \u793A\u4F8B:\u9996\u9875\u6A21\u5757
|
|
2658
|
+
\u2502 \u251C\u2500\u2500 data/ # \u6570\u636E\u5C42
|
|
2659
|
+
\u2502 \u2502 \u251C\u2500\u2500 datasources/ # \u6570\u636E\u6E90(API/\u672C\u5730)
|
|
2660
|
+
\u2502 \u2502 \u251C\u2500\u2500 models/ # \u6570\u636E\u6A21\u578B(DTO)
|
|
2661
|
+
\u2502 \u2502 \u2514\u2500\u2500 repositories/ # Repository \u5B9E\u73B0
|
|
2662
|
+
\u2502 \u251C\u2500\u2500 domain/ # \u9886\u57DF\u5C42
|
|
2663
|
+
\u2502 \u2502 \u251C\u2500\u2500 entities/ # \u4E1A\u52A1\u5B9E\u4F53
|
|
2664
|
+
\u2502 \u2502 \u251C\u2500\u2500 repositories/ # Repository \u63A5\u53E3
|
|
2665
|
+
\u2502 \u2502 \u2514\u2500\u2500 usecases/ # \u7528\u4F8B(\u4E1A\u52A1\u903B\u8F91)
|
|
2666
|
+
\u2502 \u2514\u2500\u2500 presentation/ # \u8868\u73B0\u5C42
|
|
2667
|
+
\u2502 \u251C\u2500\u2500 pages/ # \u9875\u9762
|
|
2668
|
+
\u2502 \u251C\u2500\u2500 widgets/ # \u6A21\u5757\u4E13\u7528\u7EC4\u4EF6
|
|
2669
|
+
\u2502 \u2514\u2500\u2500 viewmodels/ # \u89C6\u56FE\u6A21\u578B
|
|
2670
|
+
\u251C\u2500\u2500 \u{1F4C1} shared/ # \u5171\u4EAB\u8D44\u6E90
|
|
2671
|
+
\u2502 \u251C\u2500\u2500 widgets/ # \u901A\u7528\u7EC4\u4EF6
|
|
2672
|
+
\u2502 \u251C\u2500\u2500 components/ # \u901A\u7528\u590D\u5408\u7EC4\u4EF6
|
|
2673
|
+
\u2502 \u2514\u2500\u2500 extensions/ # \u6269\u5C55\u65B9\u6CD5
|
|
2674
|
+
\u2514\u2500\u2500 \u{1F4C1} config/ # \u914D\u7F6E\u5C42
|
|
2675
|
+
\u251C\u2500\u2500 routes/ # \u8DEF\u7531\u914D\u7F6E
|
|
2676
|
+
\u2514\u2500\u2500 theme/ # \u4E3B\u9898\u914D\u7F6E
|
|
2677
|
+
`
|
|
2678
|
+
},
|
|
2679
|
+
native: {
|
|
2680
|
+
name: "Native",
|
|
2681
|
+
displayName: "Native - Flutter \u539F\u751F\u9ED8\u8BA4\u6A21\u677F",
|
|
2682
|
+
description: "\u4EC5\u6267\u884C flutter create\uFF0C\u4E0D\u6CE8\u5165 flu-cli \u7684\u4EFB\u4F55\u57FA\u7840\u8BBE\u65BD\u5C42",
|
|
2683
|
+
repo: "",
|
|
2684
|
+
branch: "main",
|
|
2685
|
+
complexity: 0,
|
|
2686
|
+
teamSize: "N/A",
|
|
2687
|
+
codeSize: "N/A",
|
|
2688
|
+
features: ["\u2705 \u5B98\u65B9\u539F\u751F\u7ED3\u6784", "\u2705 \u65E0\u989D\u5916\u4F9D\u8D56", "\u2705 \u7EAF\u51C0\u73AF\u5883"],
|
|
2689
|
+
structure: `
|
|
2690
|
+
lib/
|
|
2691
|
+
\u2514\u2500\u2500 \u{1F4C4} main.dart # \u5B98\u65B9\u9ED8\u8BA4 Counter Demo
|
|
2692
|
+
`
|
|
2693
|
+
}
|
|
2694
|
+
};
|
|
2695
|
+
var TemplateManager = class _TemplateManager {
|
|
2696
|
+
static instance;
|
|
2697
|
+
configManager;
|
|
2698
|
+
cacheDir;
|
|
2699
|
+
constructor() {
|
|
2700
|
+
this.configManager = ConfigManager.getInstance();
|
|
2701
|
+
this.cacheDir = join15(homedir2(), ".flu-cli", "templates");
|
|
2702
|
+
}
|
|
2703
|
+
static getInstance() {
|
|
2704
|
+
if (!_TemplateManager.instance) {
|
|
2705
|
+
_TemplateManager.instance = new _TemplateManager();
|
|
2706
|
+
}
|
|
2707
|
+
return _TemplateManager.instance;
|
|
2708
|
+
}
|
|
2709
|
+
/**
|
|
2710
|
+
* 获取模板缓存目录
|
|
2711
|
+
*/
|
|
2712
|
+
getCacheDir() {
|
|
2713
|
+
return this.cacheDir;
|
|
2714
|
+
}
|
|
2715
|
+
/**
|
|
2716
|
+
* 确保缓存目录存在
|
|
2717
|
+
*/
|
|
2718
|
+
async ensureCacheDir() {
|
|
2719
|
+
if (!await fsx4.pathExists(this.cacheDir)) {
|
|
2720
|
+
await fsx4.mkdirp(this.cacheDir);
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
/**
|
|
2724
|
+
* 获取模板本地路径
|
|
2725
|
+
* @param templateName 模板名称
|
|
2726
|
+
* @param isCustom 是否为自定义模板
|
|
2727
|
+
*/
|
|
2728
|
+
getTemplatePath(templateName, isCustom = false) {
|
|
2729
|
+
const prefix = isCustom ? "custom-" : "template_";
|
|
2730
|
+
return join15(this.cacheDir, `${prefix}${templateName}`);
|
|
2731
|
+
}
|
|
2732
|
+
/**
|
|
2733
|
+
* 准备模板(确保已缓存且为最新)
|
|
2734
|
+
*
|
|
2735
|
+
* 支持以下模板源(优先级从高到低):
|
|
2736
|
+
* 1. 环境变量指定的本地模板目录 (FLU_CLI_USE_LOCAL_TEMPLATES=true + FLU_CLI_LOCAL_TEMPLATES_DIR)
|
|
2737
|
+
* 2. 自定义模板配置 (ConfigManager)
|
|
2738
|
+
* 3. 内置模板 (lite/modular/clean)
|
|
2739
|
+
*/
|
|
2740
|
+
async prepareTemplate(templateId, logger2 = new ConsoleLogger(), forceUpdate = false) {
|
|
2741
|
+
loadEnvFile();
|
|
2742
|
+
await this.ensureCacheDir();
|
|
2743
|
+
const nodeEnv = process.env.NODE_ENV || "production";
|
|
2744
|
+
const isDevelopment = nodeEnv === "development";
|
|
2745
|
+
if (isDevelopment) {
|
|
2746
|
+
const localTemplatesDir = process.env.FLU_CLI_LOCAL_TEMPLATES_DIR;
|
|
2747
|
+
if (localTemplatesDir) {
|
|
2748
|
+
if (isAbsolute(templateId) && await fsx4.pathExists(templateId)) {
|
|
2749
|
+
return templateId;
|
|
2750
|
+
}
|
|
2751
|
+
let resolvedPath = localTemplatesDir;
|
|
2752
|
+
if (!localTemplatesDir.startsWith("/") && !localTemplatesDir.match(/^[A-Z]:/)) {
|
|
2753
|
+
resolvedPath = resolve(process.cwd(), localTemplatesDir);
|
|
2754
|
+
}
|
|
2755
|
+
const localTemplatePath = join15(
|
|
2756
|
+
resolvedPath,
|
|
2757
|
+
`template_${templateId.toLowerCase()}`
|
|
2758
|
+
);
|
|
2759
|
+
if (await fsx4.pathExists(localTemplatePath)) {
|
|
2760
|
+
return localTemplatePath;
|
|
2761
|
+
}
|
|
2762
|
+
logger2.warn(`[DEV] \u672C\u5730\u6A21\u677F\u4E0D\u5B58\u5728: ${localTemplatePath}`);
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
if (BUILTIN_TEMPLATES[templateId.toLowerCase()]) {
|
|
2766
|
+
const config = BUILTIN_TEMPLATES[templateId.toLowerCase()];
|
|
2767
|
+
const targetDir = this.getTemplatePath(
|
|
2768
|
+
templateId.toLowerCase(),
|
|
2769
|
+
false
|
|
2770
|
+
);
|
|
2771
|
+
return this.ensureGitRepoCached(
|
|
2772
|
+
targetDir,
|
|
2773
|
+
config.repo,
|
|
2774
|
+
config.branch,
|
|
2775
|
+
logger2,
|
|
2776
|
+
forceUpdate
|
|
2777
|
+
);
|
|
2778
|
+
}
|
|
2779
|
+
const customTemplate = this.configManager.getTemplate(templateId);
|
|
2780
|
+
if (customTemplate) {
|
|
2781
|
+
if (customTemplate.type === "local" && customTemplate.path) {
|
|
2782
|
+
if (await fsx4.pathExists(customTemplate.path)) {
|
|
2783
|
+
return customTemplate.path;
|
|
2784
|
+
}
|
|
2785
|
+
logger2.error(`\u672C\u5730\u6A21\u677F\u8DEF\u5F84\u4E0D\u5B58\u5728: ${customTemplate.path}`);
|
|
2786
|
+
return void 0;
|
|
2787
|
+
}
|
|
2788
|
+
if (customTemplate.type === "git" && customTemplate.url) {
|
|
2789
|
+
const targetDir = this.getTemplatePath(customTemplate.id, true);
|
|
2790
|
+
return this.ensureGitRepoCached(
|
|
2791
|
+
targetDir,
|
|
2792
|
+
customTemplate.url,
|
|
2793
|
+
customTemplate.branch || "main",
|
|
2794
|
+
logger2,
|
|
2795
|
+
forceUpdate
|
|
2796
|
+
);
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
logger2.error(`\u65E0\u6CD5\u8BC6\u522B\u7684\u6A21\u677F: ${templateId}`);
|
|
2800
|
+
return void 0;
|
|
2801
|
+
}
|
|
2802
|
+
/**
|
|
2803
|
+
* 通用 Git 仓库缓存逻辑
|
|
2804
|
+
*/
|
|
2805
|
+
async ensureGitRepoCached(targetDir, repoUrl, branch, logger2, forceUpdate = false) {
|
|
2806
|
+
try {
|
|
2807
|
+
if (await fsx4.pathExists(targetDir)) {
|
|
2808
|
+
const gitDir = join15(targetDir, ".git");
|
|
2809
|
+
if (await fsx4.pathExists(gitDir)) {
|
|
2810
|
+
if (forceUpdate) {
|
|
2811
|
+
logger2.info(`\u6B63\u5728\u66F4\u65B0\u6A21\u677F: ${repoUrl}`);
|
|
2812
|
+
const git = simpleGit(targetDir);
|
|
2813
|
+
await git.fetch();
|
|
2814
|
+
await git.reset(["--hard", `origin/${branch}`]);
|
|
2815
|
+
await git.clean("f", ["-d", "-x"]);
|
|
2816
|
+
logger2.success(`\u6A21\u677F\u5DF2\u66F4\u65B0\u5230\u6700\u65B0\u7248\u672C`);
|
|
2817
|
+
} else {
|
|
2818
|
+
logger2.info(`\u4F7F\u7528\u7F13\u5B58\u6A21\u677F: ${targetDir}`);
|
|
2819
|
+
}
|
|
2820
|
+
} else {
|
|
2821
|
+
await fsx4.remove(targetDir);
|
|
2822
|
+
await this.cloneRepo(repoUrl, targetDir, branch, logger2);
|
|
2823
|
+
}
|
|
2824
|
+
return targetDir;
|
|
2825
|
+
}
|
|
2826
|
+
await this.cloneRepo(repoUrl, targetDir, branch, logger2);
|
|
2827
|
+
return targetDir;
|
|
2828
|
+
} catch (error) {
|
|
2829
|
+
logger2.error(`\u83B7\u53D6\u6A21\u677F\u5931\u8D25: ${error.message}`);
|
|
2830
|
+
return void 0;
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
/**
|
|
2834
|
+
* 克隆仓库
|
|
2835
|
+
*/
|
|
2836
|
+
async cloneRepo(repoUrl, targetDir, branch, logger2) {
|
|
2837
|
+
logger2.info(`\u6B63\u5728\u4E0B\u8F7D\u6A21\u677F: ${repoUrl} (${branch})...`);
|
|
2838
|
+
const git = simpleGit();
|
|
2839
|
+
await git.clone(repoUrl, targetDir, [
|
|
2840
|
+
"--depth",
|
|
2841
|
+
"1",
|
|
2842
|
+
"--branch",
|
|
2843
|
+
branch
|
|
2844
|
+
]);
|
|
2845
|
+
logger2.success(`\u6A21\u677F\u4E0B\u8F7D\u6210\u529F`);
|
|
2846
|
+
}
|
|
2847
|
+
/**
|
|
2848
|
+
* 检查模板更新状态
|
|
2849
|
+
*/
|
|
2850
|
+
async checkUpdate(templateId) {
|
|
2851
|
+
const templatePath = await this.prepareTemplate(
|
|
2852
|
+
templateId,
|
|
2853
|
+
new ConsoleLogger(),
|
|
2854
|
+
false
|
|
2855
|
+
);
|
|
2856
|
+
if (!templatePath) return { hasUpdate: false, message: "\u6A21\u677F\u4E0D\u5B58\u5728" };
|
|
2857
|
+
if (!await fsx4.pathExists(join15(templatePath, ".git"))) {
|
|
2858
|
+
return { hasUpdate: false, message: "\u975E Git \u6A21\u677F\uFF0C\u65E0\u6CD5\u68C0\u67E5\u66F4\u65B0" };
|
|
2859
|
+
}
|
|
2860
|
+
try {
|
|
2861
|
+
const git = simpleGit(templatePath);
|
|
2862
|
+
await git.fetch();
|
|
2863
|
+
const status = await git.status();
|
|
2864
|
+
if (status.behind > 0) {
|
|
2865
|
+
return {
|
|
2866
|
+
hasUpdate: true,
|
|
2867
|
+
message: `\u53D1\u73B0 ${status.behind} \u4E2A\u65B0\u63D0\u4EA4`
|
|
2868
|
+
};
|
|
2869
|
+
}
|
|
2870
|
+
return { hasUpdate: false, message: "\u5DF2\u662F\u6700\u65B0\u7248\u672C" };
|
|
2871
|
+
} catch (error) {
|
|
2872
|
+
return { hasUpdate: false, message: `\u68C0\u67E5\u5931\u8D25: ${error.message}` };
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
};
|
|
2876
|
+
|
|
2877
|
+
// src/generators/tasks/project_task.ts
|
|
2878
|
+
function createDefaultContext(projectPath, projectName, options = {}, logger2 = new ConsoleLogger()) {
|
|
2879
|
+
let rawTemplateType = options.templateType || options.template || "lite";
|
|
2880
|
+
if (typeof rawTemplateType === "string" && rawTemplateType.includes("/")) {
|
|
2881
|
+
const match = rawTemplateType.match(/template[_-](lite|modular|clean)/i);
|
|
2882
|
+
rawTemplateType = match ? match[1] : "lite";
|
|
2883
|
+
}
|
|
2884
|
+
const templateType = rawTemplateType.toLowerCase();
|
|
2885
|
+
const rawIncludeNetwork = options.includeNetworkLayer !== void 0 ? options.includeNetworkLayer : options.includeNetwork ?? true;
|
|
2886
|
+
const includeNetworkLayer = typeof rawIncludeNetwork === "string" ? rawIncludeNetwork === "true" : !!rawIncludeNetwork;
|
|
2887
|
+
const stateManager = (options.stateManager || "provider").toLowerCase();
|
|
2888
|
+
return {
|
|
2889
|
+
projectPath,
|
|
2890
|
+
projectName,
|
|
2891
|
+
templateType,
|
|
2892
|
+
includeNetworkLayer,
|
|
2893
|
+
stateManager,
|
|
2894
|
+
options: {
|
|
2895
|
+
...options,
|
|
2896
|
+
templateType,
|
|
2897
|
+
includeNetworkLayer
|
|
2898
|
+
},
|
|
2899
|
+
variables: {
|
|
2900
|
+
projectName,
|
|
2901
|
+
...options.variables || {}
|
|
2902
|
+
},
|
|
2903
|
+
logger: logger2
|
|
2904
|
+
};
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
// src/generators/tasks/pipeline.ts
|
|
2908
|
+
import chalk from "chalk";
|
|
2909
|
+
var ProjectPipeline = class {
|
|
2910
|
+
tasks = [];
|
|
2911
|
+
/**
|
|
2912
|
+
* 添加任务
|
|
2913
|
+
*/
|
|
2914
|
+
addTask(task) {
|
|
2915
|
+
this.tasks.push(task);
|
|
2916
|
+
return this;
|
|
2917
|
+
}
|
|
2918
|
+
/**
|
|
2919
|
+
* 执行所有任务
|
|
2920
|
+
*/
|
|
2921
|
+
async execute(context) {
|
|
2922
|
+
context.logger.info(chalk.cyan(`
|
|
2923
|
+
\u{1F680} \u5F00\u59CB\u6267\u884C\u9879\u76EE\u751F\u6210\u6D41\u6C34\u7EBF: ${context.projectName}`));
|
|
2924
|
+
context.logger.info(chalk.gray(`\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
|
|
2925
|
+
try {
|
|
2926
|
+
for (const task of this.tasks) {
|
|
2927
|
+
context.logger.info(chalk.blue(`\u{1F539} \u6267\u884C\u4EFB\u52A1: ${task.name}...`));
|
|
2928
|
+
await task.run(context);
|
|
2929
|
+
}
|
|
2930
|
+
context.logger.info(chalk.gray(`\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
|
|
2931
|
+
context.logger.success(chalk.green(`\u{1F3C1} \u6D41\u6C34\u7EBF\u6267\u884C\u6210\u529F!
|
|
2932
|
+
`));
|
|
2933
|
+
return true;
|
|
2934
|
+
} catch (error) {
|
|
2935
|
+
context.logger.error(`
|
|
2936
|
+
\u274C \u4EFB\u52A1\u6267\u884C\u5931\u8D25 [${error.message}]`);
|
|
2937
|
+
return false;
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
};
|
|
2941
|
+
|
|
2942
|
+
// src/generators/tasks/template_copy_task.ts
|
|
2943
|
+
import { join as join16 } from "path";
|
|
2944
|
+
import fsx5 from "fs-extra";
|
|
2945
|
+
var TemplateCopyTask = class {
|
|
2946
|
+
name = "\u590D\u5236\u9AA8\u67B6\u6A21\u677F";
|
|
2947
|
+
async run(context) {
|
|
2948
|
+
const { projectPath, options, logger: logger2 } = context;
|
|
2949
|
+
const sourcePath = options.templateSource;
|
|
2950
|
+
if (!sourcePath) {
|
|
2951
|
+
throw new Error("\u672A\u6307\u5B9A\u6A21\u677F\u6E90\u8DEF\u5F84");
|
|
2952
|
+
}
|
|
2953
|
+
logger2.info(`\u{1F4DD} \u6B63\u5728\u4ECE ${sourcePath} \u5B9E\u4F8B\u5316\u9879\u76EE...`);
|
|
2954
|
+
const success = await copyTemplate(sourcePath, projectPath, {
|
|
2955
|
+
excludes: ["lib/core"]
|
|
2956
|
+
});
|
|
2957
|
+
if (!success) {
|
|
2958
|
+
throw new Error("\u6A21\u677F\u6587\u4EF6\u590D\u5236\u5931\u8D25");
|
|
2959
|
+
}
|
|
2960
|
+
const scriptPath = join16(projectPath, "scripts");
|
|
2961
|
+
if (await fsx5.pathExists(scriptPath)) {
|
|
2962
|
+
await fsx5.remove(scriptPath);
|
|
2963
|
+
logger2.info(" \u2713 \u5DF2\u79FB\u9664\u6A21\u677F\u521D\u59CB\u5316\u811A\u672C");
|
|
2964
|
+
}
|
|
2965
|
+
await cleanupTemplateFiles(projectPath);
|
|
2966
|
+
logger2.info(" \u2713 \u9AA8\u67B6\u590D\u5236\u5B8C\u6210");
|
|
2967
|
+
}
|
|
2968
|
+
};
|
|
2969
|
+
|
|
2970
|
+
// src/generators/tasks/variables_replace_task.ts
|
|
2971
|
+
var VariablesReplaceTask = class {
|
|
2972
|
+
name = "\u53D8\u91CF\u81EA\u52A8\u843D\u5730";
|
|
2973
|
+
async run(context) {
|
|
2974
|
+
const { projectPath, projectName, variables, logger: logger2 } = context;
|
|
2975
|
+
logger2.info("\u{1F4DD} \u6B63\u5728\u6267\u884C\u9879\u76EE\u5168\u5C40\u53D8\u91CF\u66FF\u6362...");
|
|
2976
|
+
const smName = (context.options.stateManager || "provider").toLowerCase();
|
|
2977
|
+
const templateType = (context.options.templateType || "lite").toLowerCase();
|
|
2978
|
+
const renderData = {
|
|
2979
|
+
projectName,
|
|
2980
|
+
packageName: context.options.packageName || `com.example.${projectName}`,
|
|
2981
|
+
author: variables.author || "Your Name",
|
|
2982
|
+
year: (/* @__PURE__ */ new Date()).getFullYear().toString(),
|
|
2983
|
+
// 状态管理标识
|
|
2984
|
+
stateManager: smName,
|
|
2985
|
+
if_getx: smName === "getx",
|
|
2986
|
+
if_provider: smName === "provider",
|
|
2987
|
+
if_bloc: smName === "bloc",
|
|
2988
|
+
if_riverpod: smName === "riverpod",
|
|
2989
|
+
// 模板类型标识
|
|
2990
|
+
templateType,
|
|
2991
|
+
if_lite: templateType.includes("lite"),
|
|
2992
|
+
if_modular: templateType.includes("modular"),
|
|
2993
|
+
if_clean: templateType.includes("clean"),
|
|
2994
|
+
// 用于 pubspec.yaml 的依赖注入 (简单处理)
|
|
2995
|
+
state_manager_dependency: this.getStateManagerDependency(smName)
|
|
2996
|
+
};
|
|
2997
|
+
await TemplateRenderer.renderDirectory(projectPath, renderData);
|
|
2998
|
+
await ensurePubspecName(projectPath, projectName);
|
|
2999
|
+
logger2.info(" \u2713 \u53D8\u91CF\u66FF\u6362\u4E0E\u6A21\u677F\u6E32\u67D3\u5B8C\u6210");
|
|
3000
|
+
}
|
|
3001
|
+
getStateManagerDependency(sm) {
|
|
3002
|
+
switch (sm) {
|
|
3003
|
+
case "getx":
|
|
3004
|
+
return "get: ^4.6.6";
|
|
3005
|
+
case "provider":
|
|
3006
|
+
return "provider: ^6.1.1";
|
|
3007
|
+
case "riverpod":
|
|
3008
|
+
return "flutter_riverpod: ^2.4.9";
|
|
3009
|
+
case "bloc":
|
|
3010
|
+
return "flutter_bloc: ^8.1.3";
|
|
3011
|
+
default:
|
|
3012
|
+
return "";
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
};
|
|
3016
|
+
|
|
3017
|
+
// src/utils/pubspec_editor.ts
|
|
3018
|
+
import { join as join17 } from "path";
|
|
3019
|
+
import fsx6 from "fs-extra";
|
|
3020
|
+
var PubspecEditor = class {
|
|
3021
|
+
projectPath;
|
|
3022
|
+
logger;
|
|
3023
|
+
constructor(projectPath, logger2) {
|
|
3024
|
+
this.projectPath = projectPath;
|
|
3025
|
+
this.logger = logger2;
|
|
3026
|
+
}
|
|
3027
|
+
/**
|
|
3028
|
+
* 添加依赖
|
|
3029
|
+
* @param deps 依赖列表,如 ['provider: ^6.0.0']
|
|
3030
|
+
* @param isDev 是否为 dev_dependencies (默认 false)
|
|
3031
|
+
*/
|
|
3032
|
+
async addDependencies(deps, isDev = false) {
|
|
3033
|
+
if (deps.length === 0) return;
|
|
3034
|
+
const pubspecPath = join17(this.projectPath, "pubspec.yaml");
|
|
3035
|
+
if (!fsx6.existsSync(pubspecPath)) {
|
|
3036
|
+
this.logger?.error(`Pubspec.yaml not found at ${pubspecPath}`);
|
|
3037
|
+
return;
|
|
3038
|
+
}
|
|
3039
|
+
let content = await fsx6.readFile(pubspecPath, "utf8");
|
|
3040
|
+
const lines = content.split("\n");
|
|
3041
|
+
const sectionKey = isDev ? "dev_dependencies:" : "dependencies:";
|
|
3042
|
+
const sectionIndex = lines.findIndex((l) => l.trim() === sectionKey);
|
|
3043
|
+
if (sectionIndex === -1) {
|
|
3044
|
+
this.logger?.warn(`Could not find ${sectionKey} in pubspec.yaml`);
|
|
3045
|
+
return;
|
|
3046
|
+
}
|
|
3047
|
+
let indent = " ";
|
|
3048
|
+
for (let i = sectionIndex + 1; i < lines.length; i++) {
|
|
3049
|
+
const line = lines[i];
|
|
3050
|
+
if (line.trim() && !line.trim().startsWith("#")) {
|
|
3051
|
+
const match = line.match(/^(\s+)/);
|
|
3052
|
+
if (match) {
|
|
3053
|
+
indent = match[1];
|
|
3054
|
+
}
|
|
3055
|
+
break;
|
|
3056
|
+
}
|
|
3057
|
+
if (line.trim() && !line.startsWith(" ")) break;
|
|
3058
|
+
}
|
|
3059
|
+
let lastDepLineIndex = sectionIndex;
|
|
3060
|
+
for (let i = sectionIndex + 1; i < lines.length; i++) {
|
|
3061
|
+
const line = lines[i];
|
|
3062
|
+
if (line.trim() === "" || line.trim().startsWith("#") || line.startsWith(" ") || line.startsWith(" ")) {
|
|
3063
|
+
if (line.trim() !== "") {
|
|
3064
|
+
lastDepLineIndex = i;
|
|
3065
|
+
}
|
|
3066
|
+
continue;
|
|
3067
|
+
}
|
|
3068
|
+
break;
|
|
3069
|
+
}
|
|
3070
|
+
const newLines = [...lines];
|
|
3071
|
+
let insertOffset = 0;
|
|
3072
|
+
for (const dep of deps) {
|
|
3073
|
+
const [name] = dep.split(":");
|
|
3074
|
+
if (content.includes(`${name}:`)) {
|
|
3075
|
+
this.logger?.info(` - \u4F9D\u8D56\u5DF2\u5B58\u5728: ${name}`);
|
|
3076
|
+
continue;
|
|
3077
|
+
}
|
|
3078
|
+
const newLine = `${indent}${dep}`;
|
|
3079
|
+
newLines.splice(lastDepLineIndex + 1 + insertOffset, 0, newLine);
|
|
3080
|
+
insertOffset++;
|
|
3081
|
+
this.logger?.info(` + \u6DFB\u52A0\u4F9D\u8D56: ${name} (to ${sectionKey})`);
|
|
3082
|
+
}
|
|
3083
|
+
await fsx6.writeFile(pubspecPath, newLines.join("\n"));
|
|
3084
|
+
}
|
|
3085
|
+
};
|
|
3086
|
+
|
|
3087
|
+
// src/generators/tasks/core_enrich_task.ts
|
|
3088
|
+
import { join as join18 } from "path";
|
|
3089
|
+
import fsx7 from "fs-extra";
|
|
3090
|
+
var CoreEnrichTask = class {
|
|
3091
|
+
name = "\u6838\u5FC3\u6A21\u5757\u751F\u6210";
|
|
3092
|
+
async run(context) {
|
|
3093
|
+
const { projectPath, logger: logger2, templateType, includeNetworkLayer } = context;
|
|
3094
|
+
logger2.info("\u{1F3D7}\uFE0F \u6B63\u5728\u6784\u5EFA\u6838\u5FC3\u6A21\u5757 (Core Module)...");
|
|
3095
|
+
logger2.info(`[DEBUG] Network: ${includeNetworkLayer}`);
|
|
3096
|
+
logger2.info(`[DEBUG] Template Type: ${templateType}`);
|
|
3097
|
+
logger2.info(`[DEBUG] State Manager: ${context.stateManager}`);
|
|
3098
|
+
await copyCoreFiles(projectPath, {
|
|
3099
|
+
includeNetworkLayer,
|
|
3100
|
+
templateType: (templateType || "lite").toLowerCase(),
|
|
3101
|
+
projectName: context.projectName,
|
|
3102
|
+
stateManager: context.stateManager
|
|
3103
|
+
});
|
|
3104
|
+
const baseDeps = [
|
|
3105
|
+
"shared_preferences: ^2.2.2"
|
|
3106
|
+
];
|
|
3107
|
+
if (includeNetworkLayer) {
|
|
3108
|
+
baseDeps.push("dio: ^5.4.0");
|
|
3109
|
+
}
|
|
3110
|
+
const editor = new PubspecEditor(projectPath, logger2);
|
|
3111
|
+
await editor.addDependencies(baseDeps);
|
|
3112
|
+
const templateTypeLower = (templateType || "lite").toLowerCase();
|
|
3113
|
+
let pagesPath = "lib/pages";
|
|
3114
|
+
let vmsPath = "lib/viewmodels";
|
|
3115
|
+
let servicesPath = "lib/services";
|
|
3116
|
+
let modelsPath = "lib/models";
|
|
3117
|
+
if (templateTypeLower.includes("modular")) {
|
|
3118
|
+
pagesPath = "lib/features/home/pages";
|
|
3119
|
+
vmsPath = "lib/features/home/viewmodels";
|
|
3120
|
+
servicesPath = "lib/features/home/services";
|
|
3121
|
+
modelsPath = "lib/features/home/models";
|
|
3122
|
+
} else if (templateTypeLower.includes("clean")) {
|
|
3123
|
+
pagesPath = "lib/features/home/presentation/pages";
|
|
3124
|
+
vmsPath = "lib/features/home/presentation/viewmodels";
|
|
3125
|
+
servicesPath = "lib/features/home/data/datasources";
|
|
3126
|
+
modelsPath = "lib/features/home/data/models";
|
|
3127
|
+
}
|
|
3128
|
+
await generateIndexFile(join18(projectPath, pagesPath));
|
|
3129
|
+
await generateIndexFile(join18(projectPath, vmsPath));
|
|
3130
|
+
await generateIndexFile(join18(projectPath, servicesPath));
|
|
3131
|
+
await generateIndexFile(join18(projectPath, modelsPath));
|
|
3132
|
+
if (templateTypeLower.includes("clean")) {
|
|
3133
|
+
await generateIndexFile(join18(projectPath, "lib/features/home/data"));
|
|
3134
|
+
await generateIndexFile(join18(projectPath, "lib/features/home/presentation"));
|
|
3135
|
+
}
|
|
3136
|
+
if (includeNetworkLayer) {
|
|
3137
|
+
logger2.info("\u{1F4E1} \u6B63\u5728\u6CE8\u5165\u7F51\u7EDC\u5C42\u4E0E\u6F14\u793A\u4EE3\u7801...");
|
|
3138
|
+
await injectNetworkExamples(projectPath, {
|
|
3139
|
+
projectName: context.projectName,
|
|
3140
|
+
stateManager: context.stateManager,
|
|
3141
|
+
templateType: templateTypeLower
|
|
3142
|
+
// 传递小写的模板类型
|
|
3143
|
+
});
|
|
3144
|
+
logger2.info(" \u2713 \u7F51\u7EDC\u5C42\u4E0E\u6F14\u793A\u4EE3\u7801\u6CE8\u5165\u5B8C\u6210");
|
|
3145
|
+
} else {
|
|
3146
|
+
logger2.info(" \u2713 \u6838\u5FC3\u6A21\u5757\u751F\u6210\u5B8C\u6210 (\u4E0D\u542B\u7F51\u7EDC\u5C42)");
|
|
3147
|
+
}
|
|
3148
|
+
await this.copySnippets(projectPath, logger2);
|
|
3149
|
+
}
|
|
3150
|
+
/**
|
|
3151
|
+
* 复制 VSCode 代码片段文件到项目
|
|
3152
|
+
*/
|
|
3153
|
+
async copySnippets(projectPath, logger2) {
|
|
3154
|
+
try {
|
|
3155
|
+
const sourceFile = join18(getCoreFilesDir(), "../snippets/flu-cli.code-snippets");
|
|
3156
|
+
const targetDir = join18(projectPath, ".vscode");
|
|
3157
|
+
const targetFile = join18(targetDir, "flu-cli.code-snippets");
|
|
3158
|
+
const oldFile = join18(targetDir, "dart.code-snippets");
|
|
3159
|
+
await fsx7.ensureDir(targetDir);
|
|
3160
|
+
if (await fsx7.pathExists(sourceFile)) {
|
|
3161
|
+
await fsx7.copyFile(sourceFile, targetFile);
|
|
3162
|
+
logger2.info(" \u2713 VSCode \u4EE3\u7801\u7247\u6BB5\u5DF2\u590D\u5236");
|
|
3163
|
+
if (await fsx7.pathExists(oldFile)) {
|
|
3164
|
+
await fsx7.remove(oldFile);
|
|
3165
|
+
logger2.info(" \u2713 \u5DF2\u6E05\u7406\u65E7\u7684\u4EE3\u7801\u7247\u6BB5\u6587\u4EF6");
|
|
3166
|
+
}
|
|
3167
|
+
} else {
|
|
3168
|
+
logger2.warn(" \u26A0 \u4EE3\u7801\u7247\u6BB5\u6587\u4EF6\u672A\u627E\u5230,\u8DF3\u8FC7\u590D\u5236");
|
|
3169
|
+
}
|
|
3170
|
+
} catch (error) {
|
|
3171
|
+
logger2.warn(` \u26A0 \u590D\u5236\u4EE3\u7801\u7247\u6BB5\u5931\u8D25: ${error.message}`);
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
};
|
|
3175
|
+
|
|
3176
|
+
// src/utils/flutterHelper.ts
|
|
3177
|
+
import { exec } from "child_process";
|
|
3178
|
+
import { promisify } from "util";
|
|
3179
|
+
var execAsync = promisify(exec);
|
|
3180
|
+
async function runFlutterCreate(projectDir, projectName, packageName, flutterTemplate = "app") {
|
|
3181
|
+
try {
|
|
3182
|
+
const command = `flutter create --project-name ${projectName} --org ${packageName} --template ${flutterTemplate} ${projectDir}`;
|
|
3183
|
+
const { stdout, stderr } = await execAsync(command);
|
|
3184
|
+
if (stderr && !stderr.includes("Warning")) {
|
|
3185
|
+
logger.warn(stderr);
|
|
3186
|
+
}
|
|
3187
|
+
return stdout.includes("All done!") || stdout.includes("Created project");
|
|
3188
|
+
} catch (error) {
|
|
3189
|
+
logger.error(`Flutter create \u5931\u8D25: ${error.message}`);
|
|
3190
|
+
return false;
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
async function runFlutterPubGet(projectDir) {
|
|
3194
|
+
try {
|
|
3195
|
+
const { stdout, stderr } = await execAsync("flutter pub get", {
|
|
3196
|
+
cwd: projectDir
|
|
3197
|
+
});
|
|
3198
|
+
if (stderr) {
|
|
3199
|
+
logger.warn(stderr);
|
|
3200
|
+
}
|
|
3201
|
+
return true;
|
|
3202
|
+
} catch (error) {
|
|
3203
|
+
logger.error(`Flutter pub get \u5931\u8D25: ${error.message}`);
|
|
3204
|
+
return false;
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
async function checkFlutterInstalled() {
|
|
3208
|
+
try {
|
|
3209
|
+
await execAsync("flutter --version");
|
|
3210
|
+
return true;
|
|
3211
|
+
} catch (error) {
|
|
3212
|
+
return false;
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
async function getFlutterVersion() {
|
|
3216
|
+
try {
|
|
3217
|
+
const { stdout } = await execAsync("flutter --version");
|
|
3218
|
+
const match = stdout.match(/Flutter (\d+\.\d+\.\d+)/);
|
|
3219
|
+
return match ? match[1] : null;
|
|
3220
|
+
} catch (error) {
|
|
3221
|
+
return null;
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3225
|
+
// src/generators/tasks/flutter_init_task.ts
|
|
3226
|
+
var FlutterInitTask = class {
|
|
3227
|
+
name = "\u539F\u751F Flutter \u521D\u59CB\u5316";
|
|
3228
|
+
async run(context) {
|
|
3229
|
+
const { projectPath, projectName, options, logger: logger2 } = context;
|
|
3230
|
+
if (!options.createFlutterProject && context.templateType !== "native") {
|
|
3231
|
+
return;
|
|
3232
|
+
}
|
|
3233
|
+
logger2.info(`\u{1F528} \u6B63\u5728\u521D\u59CB\u5316\u539F\u751F Flutter \u73AF\u5883 (${options.flutterTemplate || "app"})...`);
|
|
3234
|
+
const org = options.packageName ? options.packageName.split(".").slice(0, -1).join(".") : "com.example";
|
|
3235
|
+
const success = await runFlutterCreate(
|
|
3236
|
+
projectPath,
|
|
3237
|
+
projectName,
|
|
3238
|
+
org,
|
|
3239
|
+
options.flutterTemplate || "app"
|
|
3240
|
+
);
|
|
3241
|
+
if (!success) {
|
|
3242
|
+
throw new Error("flutter create \u6267\u884C\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5 Flutter SDK \u73AF\u5883");
|
|
3243
|
+
}
|
|
3244
|
+
logger2.info(" \u2713 Native Flutter \u9879\u76EE\u521D\u59CB\u5316\u6210\u529F");
|
|
3245
|
+
}
|
|
3246
|
+
};
|
|
3247
|
+
|
|
3248
|
+
// src/generators/tasks/route_mapping_task.ts
|
|
3249
|
+
import { join as join19 } from "path";
|
|
3250
|
+
import fsx8 from "fs-extra";
|
|
3251
|
+
var RouteMappingTask = class {
|
|
3252
|
+
name = "\u4E1A\u52A1\u8DEF\u7531\u81EA\u52A8\u6CE8\u518C";
|
|
3253
|
+
async run(context) {
|
|
3254
|
+
const { projectPath, templateType, logger: logger2, stateManager } = context;
|
|
3255
|
+
try {
|
|
3256
|
+
const routesPath = join19(projectPath, "lib", "core", "router", "app_routes.dart");
|
|
3257
|
+
if (!await fsx8.pathExists(routesPath)) return;
|
|
3258
|
+
let content = await fsx8.readFile(routesPath, "utf8");
|
|
3259
|
+
const todoMarker = stateManager === "getx" ? "// --- \u5728\u6B64\u4E0B\u65B9\u6DFB\u52A0\u60A8\u7684\u81EA\u5B9A\u4E49\u8DEF\u7531 ---" : "// --- \u5728\u6B64\u4E0B\u65B9\u6DFB\u52A0\u60A8\u7684\u81EA\u5B9A\u4E49\u8DEF\u7531 ---";
|
|
3260
|
+
if (!content.includes(todoMarker)) {
|
|
3261
|
+
return;
|
|
3262
|
+
}
|
|
3263
|
+
const pages = await this.scanGeneratedPages(projectPath, templateType);
|
|
3264
|
+
if (pages.length === 0) return;
|
|
3265
|
+
let routeMappings = [];
|
|
3266
|
+
if (stateManager === "getx") {
|
|
3267
|
+
routeMappings = pages.filter((page) => !content.includes(`GetPage(name: ${page.routeName},`)).map(
|
|
3268
|
+
(page) => `GetPage(name: ${page.routeName}, page: () => const ${page.className}()),`
|
|
3269
|
+
);
|
|
3270
|
+
} else {
|
|
3271
|
+
routeMappings = pages.filter((page) => !content.includes(`${page.routeName}:`)).map(
|
|
3272
|
+
(page) => `${page.routeName}: (context) => const ${page.className}(),`
|
|
3273
|
+
);
|
|
3274
|
+
}
|
|
3275
|
+
const injection = routeMappings.join("\n ") + "\n " + todoMarker;
|
|
3276
|
+
content = content.replace(todoMarker, injection);
|
|
3277
|
+
await fsx8.writeFile(routesPath, content);
|
|
3278
|
+
logger2.info(` \u2713 \u5DF2\u81EA\u52A8\u6CE8\u518C ${pages.length} \u4E2A\u4E1A\u52A1\u8DEF\u7531`);
|
|
3279
|
+
} catch (e) {
|
|
3280
|
+
logger2.warn(` \u26A0\uFE0F \u8DEF\u7531\u81EA\u52A8\u6CE8\u518C\u8DF3\u8FC7: ${e.message}`);
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
async scanGeneratedPages(projectPath, templateType) {
|
|
3284
|
+
const pages = [];
|
|
3285
|
+
const searchPaths = [
|
|
3286
|
+
join19(projectPath, "lib", "pages"),
|
|
3287
|
+
// Lite
|
|
3288
|
+
join19(projectPath, "lib", "features", "home", "pages"),
|
|
3289
|
+
// Modular
|
|
3290
|
+
join19(projectPath, "lib", "features", "home", "presentation", "pages"),
|
|
3291
|
+
// Clean
|
|
3292
|
+
join19(projectPath, "lib", "features", "user", "presentation", "pages")
|
|
3293
|
+
// Clean
|
|
3294
|
+
];
|
|
3295
|
+
for (const dir of searchPaths) {
|
|
3296
|
+
if (!await fsx8.pathExists(dir)) continue;
|
|
3297
|
+
const files = await fsx8.readdir(dir);
|
|
3298
|
+
for (const file of files) {
|
|
3299
|
+
if (file.endsWith("_page.dart") && file !== "index.dart" && file !== "home_page.dart" && file !== "splash_page.dart") {
|
|
3300
|
+
const baseName = file.replace("_page.dart", "");
|
|
3301
|
+
const className = toPascalCase(baseName) + "Page";
|
|
3302
|
+
const routeName = baseName.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
3303
|
+
pages.push({ className, routeName });
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
return pages;
|
|
3308
|
+
}
|
|
3309
|
+
};
|
|
3310
|
+
|
|
3311
|
+
// src/generators/tasks/cleanup_task.ts
|
|
3312
|
+
import { join as join20 } from "path";
|
|
3313
|
+
import fsx9 from "fs-extra";
|
|
3314
|
+
var CleanupTask = class {
|
|
3315
|
+
name = "\u9879\u76EE\u73AF\u5883\u4FEE\u590D\u4E0E\u6E05\u7406";
|
|
3316
|
+
async run(context) {
|
|
3317
|
+
const { projectPath, logger: logger2, projectName } = context;
|
|
3318
|
+
logger2.info("\u{1F9F9} \u6B63\u5728\u6267\u884C\u6700\u540E\u7684\u517C\u5BB9\u6027\u68C0\u67E5\u4E0E\u6E05\u7406...");
|
|
3319
|
+
await this.patchMacOSEntitlements(projectPath, logger2);
|
|
3320
|
+
await this.fixTestFile(projectPath, projectName, logger2);
|
|
3321
|
+
await this.fixLegacyImports(projectPath, logger2);
|
|
3322
|
+
logger2.info(" \u2713 \u73AF\u5883\u4FEE\u590D\u5B8C\u6210");
|
|
3323
|
+
}
|
|
3324
|
+
/**
|
|
3325
|
+
* 自动修复 macOS 网络权限 (避免 SocketException)
|
|
3326
|
+
*/
|
|
3327
|
+
async patchMacOSEntitlements(projectPath, logger2) {
|
|
3328
|
+
try {
|
|
3329
|
+
const entitlementsDir = join20(projectPath, "macos", "Runner");
|
|
3330
|
+
if (!await fsx9.pathExists(entitlementsDir)) return;
|
|
3331
|
+
const debugFile = join20(entitlementsDir, "DebugProfile.entitlements");
|
|
3332
|
+
const releaseFile = join20(entitlementsDir, "Release.entitlements");
|
|
3333
|
+
const entitlementKey = "com.apple.security.network.client";
|
|
3334
|
+
const patch = async (filePath) => {
|
|
3335
|
+
if (await fsx9.pathExists(filePath)) {
|
|
3336
|
+
let content = await fsx9.readFile(filePath, "utf8");
|
|
3337
|
+
if (!content.includes(entitlementKey)) {
|
|
3338
|
+
const entitlementStr = `
|
|
3339
|
+
<key>${entitlementKey}</key>
|
|
3340
|
+
<true/>`;
|
|
3341
|
+
content = content.replace("</dict>", `${entitlementStr}
|
|
3342
|
+
</dict>`);
|
|
3343
|
+
await fsx9.writeFile(filePath, content, "utf8");
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
};
|
|
3347
|
+
await patch(debugFile);
|
|
3348
|
+
await patch(releaseFile);
|
|
3349
|
+
logger2.info(" \u2713 \u5DF2\u6CE8\u5165 macOS \u7F51\u7EDC\u8BBF\u95EE\u6743\u9650");
|
|
3350
|
+
} catch (e) {
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
/**
|
|
3354
|
+
* 修复默认测试文件中的导入路径
|
|
3355
|
+
*/
|
|
3356
|
+
async fixTestFile(projectDir, projectName, logger2) {
|
|
3357
|
+
const testFile = join20(projectDir, "test", "widget_test.dart");
|
|
3358
|
+
if (!await fsx9.pathExists(testFile)) return;
|
|
3359
|
+
try {
|
|
3360
|
+
let content = await fsx9.readFile(testFile, "utf8");
|
|
3361
|
+
if (content.includes("Counter increments smoke test")) {
|
|
3362
|
+
content = `import 'package:flutter_test/flutter_test.dart';
|
|
3363
|
+
import 'package:${projectName}/app.dart';
|
|
3364
|
+
|
|
3365
|
+
void main() {
|
|
3366
|
+
testWidgets('App smoke test', (WidgetTester tester) async {
|
|
3367
|
+
await tester.pumpWidget(const App());
|
|
3368
|
+
expect(find.byType(App), findsOneWidget);
|
|
3369
|
+
});
|
|
3370
|
+
}
|
|
3371
|
+
`;
|
|
3372
|
+
await fsx9.writeFile(testFile, content, "utf8");
|
|
3373
|
+
}
|
|
3374
|
+
} catch {
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
/**
|
|
3378
|
+
* 修复残留的旧路径引用
|
|
3379
|
+
*/
|
|
3380
|
+
async fixLegacyImports(projectPath, logger2) {
|
|
3381
|
+
const libPath = join20(projectPath, "lib");
|
|
3382
|
+
if (!await fsx9.pathExists(libPath)) return;
|
|
3383
|
+
}
|
|
3384
|
+
};
|
|
3385
|
+
|
|
3386
|
+
// src/generators/tasks/state_manager_enrich_task.ts
|
|
3387
|
+
import { join as join21 } from "path";
|
|
3388
|
+
import fsx10 from "fs-extra";
|
|
3389
|
+
var StateManagerEnrichTask = class {
|
|
3390
|
+
name = "\u72B6\u6001\u7BA1\u7406\u67B6\u6784\u589E\u5F3A";
|
|
3391
|
+
async run(context) {
|
|
3392
|
+
const { projectPath, options, logger: logger2 } = context;
|
|
3393
|
+
const smName = options.stateManager || "default";
|
|
3394
|
+
logger2.info(`\u{1F9EA} \u6B63\u5728\u6CE8\u5165 ${smName} \u72B6\u6001\u7BA1\u7406\u9002\u914D\u5668...`);
|
|
3395
|
+
const adapter = StateManagerAdapterFactory.getAdapter(smName);
|
|
3396
|
+
try {
|
|
3397
|
+
await this.enrichDependencies(projectPath, adapter.getDependencies(), logger2);
|
|
3398
|
+
const baseVM = adapter.getBaseViewModelTemplate(context);
|
|
3399
|
+
if (baseVM !== null && baseVM !== "") {
|
|
3400
|
+
await this.enrichBaseViewModel(projectPath, adapter, baseVM, logger2);
|
|
3401
|
+
}
|
|
3402
|
+
if (adapter.onEnrich) {
|
|
3403
|
+
await adapter.onEnrich(context);
|
|
3404
|
+
}
|
|
3405
|
+
await this.enrichAppEntry(projectPath, adapter, context, logger2);
|
|
3406
|
+
await this.enrichRoutes(projectPath, adapter, context, logger2);
|
|
3407
|
+
logger2.info(` \u2713 \u72B6\u6001\u7BA1\u7406\u589E\u5F3A\u5B8C\u6210 (${adapter.name})`);
|
|
3408
|
+
} catch (e) {
|
|
3409
|
+
logger2.error(` \u274C \u72B6\u6001\u7BA1\u7406\u589E\u5F3A\u5931\u8D25: ${e.message}`);
|
|
3410
|
+
throw e;
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
async enrichDependencies(projectPath, deps, logger2) {
|
|
3414
|
+
const editor = new PubspecEditor(projectPath, logger2);
|
|
3415
|
+
await editor.addDependencies(deps);
|
|
3416
|
+
}
|
|
3417
|
+
async enrichBaseViewModel(projectPath, adapter, content, logger2) {
|
|
3418
|
+
const baseDir = join21(projectPath, "lib", "core", "base");
|
|
3419
|
+
await fsx10.ensureDir(baseDir);
|
|
3420
|
+
const target = join21(baseDir, "base_viewmodel.dart");
|
|
3421
|
+
await fsx10.writeFile(target, content);
|
|
3422
|
+
logger2.info(` + \u751F\u6210 BaseViewModel (\u9002\u914D ${adapter.name})`);
|
|
3423
|
+
}
|
|
3424
|
+
async enrichAppEntry(projectPath, adapter, context, logger2) {
|
|
3425
|
+
const possiblePaths = [
|
|
3426
|
+
join21(projectPath, "lib", "app.dart"),
|
|
3427
|
+
join21(projectPath, "lib", "main.dart")
|
|
3428
|
+
];
|
|
3429
|
+
for (const path of possiblePaths) {
|
|
3430
|
+
if (await fsx10.pathExists(path)) {
|
|
3431
|
+
let content = await fsx10.readFile(path, "utf8");
|
|
3432
|
+
const patched = adapter.patchAppEntry(content, context);
|
|
3433
|
+
if (patched !== content) {
|
|
3434
|
+
await fsx10.writeFile(path, patched);
|
|
3435
|
+
logger2.info(` + \u5DF2\u589E\u5F3A App \u5165\u53E3: ${path.split("/").pop()}`);
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3440
|
+
async enrichRoutes(projectPath, adapter, context, logger2) {
|
|
3441
|
+
if (!adapter.patchRoutes) return;
|
|
3442
|
+
const routesPath = join21(projectPath, "lib", "core", "router", "app_routes.dart");
|
|
3443
|
+
if (await fsx10.pathExists(routesPath)) {
|
|
3444
|
+
let content = await fsx10.readFile(routesPath, "utf8");
|
|
3445
|
+
const patched = adapter.patchRoutes(content, context);
|
|
3446
|
+
if (patched !== content) {
|
|
3447
|
+
await fsx10.writeFile(routesPath, patched);
|
|
3448
|
+
logger2.info(` + \u5DF2\u589E\u5F3A\u8DEF\u7531\u914D\u7F6E`);
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
};
|
|
3453
|
+
|
|
3454
|
+
// src/generators/tasks/project_config_task.ts
|
|
3455
|
+
import { join as join22 } from "path";
|
|
3456
|
+
import fsx11 from "fs-extra";
|
|
3457
|
+
var ProjectConfigTask = class {
|
|
3458
|
+
name = "\u751F\u6210\u9879\u76EE\u914D\u7F6E";
|
|
3459
|
+
async run(context) {
|
|
3460
|
+
const { projectPath, templateType, options, logger: logger2 } = context;
|
|
3461
|
+
const templateSource = options.templateSource;
|
|
3462
|
+
logger2.info("\u2699\uFE0F \u6B63\u5728\u521D\u59CB\u5316\u9879\u76EE\u914D\u7F6E...");
|
|
3463
|
+
let baseConfig = {};
|
|
3464
|
+
if (templateSource) {
|
|
3465
|
+
const templatePaths = [
|
|
3466
|
+
join22(templateSource, ".flu-template.json"),
|
|
3467
|
+
join22(templateSource, ".flu-cli.json")
|
|
3468
|
+
];
|
|
3469
|
+
for (const configPath of templatePaths) {
|
|
3470
|
+
if (await fsx11.pathExists(configPath)) {
|
|
3471
|
+
try {
|
|
3472
|
+
const content = await fsx11.readFile(configPath, "utf8");
|
|
3473
|
+
baseConfig = JSON.parse(content);
|
|
3474
|
+
logger2.info(` \u2713 \u4ECE\u6A21\u677F\u4E2D\u52A0\u8F7D\u4E86\u81EA\u63CF\u8FF0\u914D\u7F6E: ${configPath.split("/").pop()}`);
|
|
3475
|
+
break;
|
|
3476
|
+
} catch (e) {
|
|
3477
|
+
logger2.warn(` \u26A0\uFE0F \u8BFB\u53D6\u6A21\u677F\u914D\u7F6E\u5931\u8D25: ${configPath}`);
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
if (!baseConfig.generators) {
|
|
3483
|
+
baseConfig = ProjectConfigManager.getDefaultConfigTemplate(templateType);
|
|
3484
|
+
logger2.info(` \u2713 \u4F7F\u7528\u5185\u7F6E\u9ED8\u8BA4\u914D\u7F6E\u65B9\u6848: ${templateType}`);
|
|
3485
|
+
}
|
|
3486
|
+
const finalConfig = {
|
|
3487
|
+
...baseConfig,
|
|
3488
|
+
template: templateType,
|
|
3489
|
+
packageName: options.packageName || baseConfig.packageName
|
|
3490
|
+
};
|
|
3491
|
+
const targetConfigPath = join22(projectPath, ".flu-cli.json");
|
|
3492
|
+
await fsx11.writeJson(targetConfigPath, finalConfig, { spaces: 2 });
|
|
3493
|
+
logger2.info(" \u2713 \u9879\u76EE\u914D\u7F6E\u6587\u4EF6\u5DF2\u751F\u6210");
|
|
3494
|
+
}
|
|
3495
|
+
};
|
|
3496
|
+
|
|
3497
|
+
// src/generators/tasks/network_example_injection_task.ts
|
|
3498
|
+
import { join as join23 } from "path";
|
|
3499
|
+
import fsx12 from "fs-extra";
|
|
3500
|
+
var NetworkExampleInjectionTask = class {
|
|
3501
|
+
name = "\u7F51\u7EDC\u793A\u4F8B\u4EE3\u7801\u6CE8\u5165";
|
|
3502
|
+
async run(context) {
|
|
3503
|
+
const { projectPath, templateType, includeNetworkLayer, logger: logger2 } = context;
|
|
3504
|
+
if (!includeNetworkLayer) {
|
|
3505
|
+
logger2.info(" \u23E9 \u8DF3\u8FC7\u7F51\u7EDC\u793A\u4F8B\u6CE8\u5165");
|
|
3506
|
+
return;
|
|
3507
|
+
}
|
|
3508
|
+
const templateTypeLower = (templateType || "lite").toLowerCase();
|
|
3509
|
+
try {
|
|
3510
|
+
await this.injectGalleryCard(projectPath, templateTypeLower, logger2);
|
|
3511
|
+
await this.injectRoutes(projectPath, templateTypeLower, logger2);
|
|
3512
|
+
logger2.info(" \u2713 \u7F51\u7EDC\u793A\u4F8B\u4EE3\u7801\u6CE8\u5165\u5B8C\u6210");
|
|
3513
|
+
} catch (error) {
|
|
3514
|
+
logger2.warn(` \u26A0 \u7F51\u7EDC\u793A\u4F8B\u6CE8\u5165\u5931\u8D25: ${error}`);
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
/**
|
|
3518
|
+
* 注入图库卡片到 HomePage
|
|
3519
|
+
*/
|
|
3520
|
+
async injectGalleryCard(projectPath, templateType, logger2) {
|
|
3521
|
+
const homePagePath = this.getHomePagePath(projectPath, templateType);
|
|
3522
|
+
if (!await fsx12.pathExists(homePagePath)) {
|
|
3523
|
+
return;
|
|
3524
|
+
}
|
|
3525
|
+
let content = await fsx12.readFile(homePagePath, "utf8");
|
|
3526
|
+
if (content.includes("_buildGalleryCard()")) {
|
|
3527
|
+
logger2.info(" \u2139 \u56FE\u5E93\u5361\u7247\u5DF2\u5B58\u5728\uFF0C\u8DF3\u8FC7\u6CE8\u5165");
|
|
3528
|
+
return;
|
|
3529
|
+
}
|
|
3530
|
+
const anchorRegex = /\/\/ 图库入口卡片 \(网络示例\) - 由 CLI 动态注入\s*/;
|
|
3531
|
+
if (!anchorRegex.test(content)) {
|
|
3532
|
+
logger2.warn(" \u26A0 \u672A\u627E\u5230\u56FE\u5E93\u5361\u7247\u6CE8\u5165\u951A\u70B9");
|
|
3533
|
+
return;
|
|
3534
|
+
}
|
|
3535
|
+
content = content.replace(
|
|
3536
|
+
anchorRegex,
|
|
3537
|
+
`_buildGalleryCard(),
|
|
3538
|
+
|
|
3539
|
+
`
|
|
3540
|
+
);
|
|
3541
|
+
const galleryCardMethod = `
|
|
3542
|
+
/// \u6784\u5EFA\u56FE\u5E93\u5165\u53E3\u5361\u7247
|
|
3543
|
+
Widget _buildGalleryCard() {
|
|
3544
|
+
return _buildEntryCard(
|
|
3545
|
+
title: '\u7CBE\u7F8E\u56FE\u5E93',
|
|
3546
|
+
subtitle: '\u5206\u9875\u52A0\u8F7D\u4E0E\u7F51\u7EDC\u8BF7\u6C42\u793A\u4F8B',
|
|
3547
|
+
icon: Icons.photo_library,
|
|
3548
|
+
color: const Color(0xFF667eea),
|
|
3549
|
+
onTap: () => NavigatorUtil.pushNamed(AppRoutes.egList),
|
|
3550
|
+
);
|
|
3551
|
+
}
|
|
3552
|
+
`;
|
|
3553
|
+
const lastBraceIndex = content.lastIndexOf("}");
|
|
3554
|
+
content = content.slice(0, lastBraceIndex) + galleryCardMethod + "\n" + content.slice(lastBraceIndex);
|
|
3555
|
+
await fsx12.writeFile(homePagePath, content, "utf8");
|
|
3556
|
+
logger2.info(" \u2713 \u56FE\u5E93\u5361\u7247\u5DF2\u6CE8\u5165\u5230 HomePage");
|
|
3557
|
+
}
|
|
3558
|
+
/**
|
|
3559
|
+
* 注入路由到 AppRoutes
|
|
3560
|
+
*/
|
|
3561
|
+
async injectRoutes(projectPath, templateType, logger2) {
|
|
3562
|
+
const routesPath = join23(projectPath, "lib", "core", "router", "app_routes.dart");
|
|
3563
|
+
if (!await fsx12.pathExists(routesPath)) {
|
|
3564
|
+
return;
|
|
3565
|
+
}
|
|
3566
|
+
let content = await fsx12.readFile(routesPath, "utf8");
|
|
3567
|
+
if (content.includes("egList = '/eg-list'")) {
|
|
3568
|
+
logger2.info(" \u2139 \u7F51\u7EDC\u793A\u4F8B\u8DEF\u7531\u5DF2\u5B58\u5728\uFF0C\u8DF3\u8FC7\u6CE8\u5165");
|
|
3569
|
+
return;
|
|
3570
|
+
}
|
|
3571
|
+
const constantAnchor = /\/\/ 网络示例路由 - 由 CLI 动态注入\s*/;
|
|
3572
|
+
if (constantAnchor.test(content)) {
|
|
3573
|
+
content = content.replace(
|
|
3574
|
+
constantAnchor,
|
|
3575
|
+
"static const String egList = '/eg-list';\n "
|
|
3576
|
+
);
|
|
3577
|
+
}
|
|
3578
|
+
const mappingAnchor = /\/\/ 网络示例路由映射 - 由 CLI 动态注入\s*/;
|
|
3579
|
+
if (mappingAnchor.test(content)) {
|
|
3580
|
+
content = content.replace(
|
|
3581
|
+
mappingAnchor,
|
|
3582
|
+
"egList: (context) => const EgListPage(),\n "
|
|
3583
|
+
);
|
|
3584
|
+
}
|
|
3585
|
+
await fsx12.writeFile(routesPath, content, "utf8");
|
|
3586
|
+
logger2.info(" \u2713 \u7F51\u7EDC\u793A\u4F8B\u8DEF\u7531\u5DF2\u6CE8\u5165\u5230 AppRoutes");
|
|
3587
|
+
}
|
|
3588
|
+
/**
|
|
3589
|
+
* 获取 HomePage 路径(适配不同模板结构)
|
|
3590
|
+
*/
|
|
3591
|
+
getHomePagePath(projectPath, templateType) {
|
|
3592
|
+
if (templateType.includes("lite")) {
|
|
3593
|
+
return join23(projectPath, "lib", "pages", "home_page.dart");
|
|
3594
|
+
} else if (templateType.includes("modular")) {
|
|
3595
|
+
return join23(projectPath, "lib", "features", "home", "pages", "home_page.dart");
|
|
3596
|
+
} else {
|
|
3597
|
+
return join23(projectPath, "lib", "features", "home", "presentation", "pages", "home_page.dart");
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3600
|
+
};
|
|
3601
|
+
|
|
3602
|
+
// src/generators/project_generator.ts
|
|
3603
|
+
var ProjectGenerator = class {
|
|
3604
|
+
/**
|
|
3605
|
+
* 生成项目
|
|
3606
|
+
* @param {string} name 项目名称
|
|
3607
|
+
* @param {object} options 选项
|
|
3608
|
+
* @param {Logger} logger 日志工具
|
|
3609
|
+
*/
|
|
3610
|
+
async generate(name, options = {}, logger2 = new ConsoleLogger()) {
|
|
3611
|
+
const templateType = options.templateType || options.template || "lite";
|
|
3612
|
+
const {
|
|
3613
|
+
outputDir = process.cwd()
|
|
3614
|
+
} = options;
|
|
3615
|
+
const projectPath = join24(outputDir, name);
|
|
3616
|
+
if (this.checkProjectExists(projectPath)) {
|
|
3617
|
+
logger2.error(`\u9879\u76EE\u5DF2\u5B58\u5728: ${projectPath}`);
|
|
3618
|
+
return false;
|
|
3619
|
+
}
|
|
3620
|
+
try {
|
|
3621
|
+
const templateManager = TemplateManager.getInstance();
|
|
3622
|
+
let templateName = typeof templateType === "string" ? templateType : "custom";
|
|
3623
|
+
if (templateName.includes("/") || templateName.includes("\\")) {
|
|
3624
|
+
const parts = templateName.split(/[/\\]/);
|
|
3625
|
+
const lastPart = parts[parts.length - 1];
|
|
3626
|
+
templateName = lastPart.replace(/^template_/, "");
|
|
3627
|
+
}
|
|
3628
|
+
const templateSource = await templateManager.prepareTemplate(
|
|
3629
|
+
templateName,
|
|
3630
|
+
logger2
|
|
3631
|
+
);
|
|
3632
|
+
const pipeline = new ProjectPipeline();
|
|
3633
|
+
const context = createDefaultContext(projectPath, name, options, logger2);
|
|
3634
|
+
context.options.templateSource = templateSource;
|
|
3635
|
+
pipeline.addTask(new FlutterInitTask()).addTask(new TemplateCopyTask()).addTask(new ProjectConfigTask()).addTask(new CoreEnrichTask()).addTask(new VariablesReplaceTask()).addTask(new StateManagerEnrichTask()).addTask(new RouteMappingTask()).addTask(new NetworkExampleInjectionTask()).addTask(new CleanupTask());
|
|
3636
|
+
const success = await pipeline.execute(context);
|
|
3637
|
+
if (success) {
|
|
3638
|
+
logger2.newLine();
|
|
3639
|
+
logger2.success(`\u2705 \u9879\u76EE ${name} \u5B9E\u4F8B\u5316\u6210\u529F\uFF01`);
|
|
3640
|
+
logger2.info(`\u4E0B\u4E00\u6B65:`);
|
|
3641
|
+
logger2.info(` cd ${name}`);
|
|
3642
|
+
logger2.info(` flutter pub get`);
|
|
3643
|
+
logger2.info(` flutter run`);
|
|
3644
|
+
}
|
|
3645
|
+
return success;
|
|
3646
|
+
} catch (error) {
|
|
3647
|
+
logger2.error(`\u274C \u521B\u5EFA\u9879\u76EE\u8FC7\u7A0B\u4E2D\u53D1\u751F\u81F4\u547D\u9519\u8BEF: ${error.message}`);
|
|
3648
|
+
return false;
|
|
3649
|
+
}
|
|
3650
|
+
}
|
|
3651
|
+
/**
|
|
3652
|
+
* 检查项目目录是否存在
|
|
3653
|
+
*/
|
|
3654
|
+
checkProjectExists(projectPath) {
|
|
3655
|
+
return existsSync13(projectPath);
|
|
3656
|
+
}
|
|
3657
|
+
};
|
|
3658
|
+
|
|
3659
|
+
// src/utils/app_assets_manager.ts
|
|
3660
|
+
import fsx13 from "fs-extra";
|
|
3661
|
+
import { join as join25, resolve as resolve2 } from "path";
|
|
3662
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync10 } from "fs";
|
|
3663
|
+
import { exec as exec2 } from "child_process";
|
|
3664
|
+
import { promisify as promisify2 } from "util";
|
|
3665
|
+
var execPromise = promisify2(exec2);
|
|
3666
|
+
var AppAssetsManager = class {
|
|
3667
|
+
/**
|
|
3668
|
+
* 设置应用资源
|
|
3669
|
+
* @param projectPath 项目路径
|
|
3670
|
+
* @param assets 资源配置
|
|
3671
|
+
* @param logger 日志工具
|
|
3672
|
+
*/
|
|
3673
|
+
async setupAppAssets(projectPath, assets, logger2) {
|
|
3674
|
+
try {
|
|
3675
|
+
const assetsDir = join25(projectPath, "assets");
|
|
3676
|
+
await fsx13.ensureDir(assetsDir);
|
|
3677
|
+
const devDeps = [];
|
|
3678
|
+
let pubspecAdditions = "";
|
|
3679
|
+
if (assets.appIcon) {
|
|
3680
|
+
logger2.info("\u{1F3A8} \u8BBE\u7F6E\u5E94\u7528\u56FE\u6807...");
|
|
3681
|
+
const iconDir = join25(assetsDir, "icon");
|
|
3682
|
+
await fsx13.ensureDir(iconDir);
|
|
3683
|
+
await this.copyIfDifferent(
|
|
3684
|
+
assets.appIcon,
|
|
3685
|
+
join25(iconDir, "app_icon.png")
|
|
3686
|
+
);
|
|
3687
|
+
devDeps.push("flutter_launcher_icons: ^0.14.4");
|
|
3688
|
+
pubspecAdditions += `
|
|
3689
|
+
flutter_launcher_icons:
|
|
3690
|
+
android: true
|
|
3691
|
+
ios: true
|
|
3692
|
+
image_path: "assets/icon/app_icon.png"
|
|
3693
|
+
min_sdk_android: 21
|
|
3694
|
+
web:
|
|
3695
|
+
generate: true
|
|
3696
|
+
image_path: "assets/icon/app_icon.png"
|
|
3697
|
+
windows:
|
|
3698
|
+
generate: true
|
|
3699
|
+
image_path: "assets/icon/app_icon.png"
|
|
3700
|
+
macos:
|
|
3701
|
+
generate: true
|
|
3702
|
+
image_path: "assets/icon/app_icon.png"
|
|
3703
|
+
`;
|
|
3704
|
+
}
|
|
3705
|
+
if (assets.splashLogo) {
|
|
3706
|
+
logger2.info("\u{1F680} \u8BBE\u7F6E\u542F\u52A8\u56FE...");
|
|
3707
|
+
const splashDir = join25(assetsDir, "splash");
|
|
3708
|
+
await fsx13.ensureDir(splashDir);
|
|
3709
|
+
await this.copyIfDifferent(
|
|
3710
|
+
assets.splashLogo,
|
|
3711
|
+
join25(splashDir, "logo.png")
|
|
3712
|
+
);
|
|
3713
|
+
let splashConfig = `
|
|
3714
|
+
flutter_native_splash:
|
|
3715
|
+
image: "assets/splash/logo.png"
|
|
3716
|
+
android: true
|
|
3717
|
+
ios: true
|
|
3718
|
+
web: true
|
|
3719
|
+
`;
|
|
3720
|
+
if (assets.splashBackgroundColor) {
|
|
3721
|
+
splashConfig += ` color: "${assets.splashBackgroundColor}"
|
|
3722
|
+
`;
|
|
3723
|
+
}
|
|
3724
|
+
if (assets.splashBackground) {
|
|
3725
|
+
await this.copyIfDifferent(
|
|
3726
|
+
assets.splashBackground,
|
|
3727
|
+
join25(splashDir, "background.png")
|
|
3728
|
+
);
|
|
3729
|
+
splashConfig += ` background_image: "assets/splash/background.png"
|
|
3730
|
+
`;
|
|
3731
|
+
}
|
|
3732
|
+
if (assets.enableDarkMode) {
|
|
3733
|
+
const darkColor = assets.splashBackgroundColorDark || "#000000";
|
|
3734
|
+
splashConfig += ` color_dark: "${darkColor}"
|
|
3735
|
+
`;
|
|
3736
|
+
if (assets.splashLogoDark) {
|
|
3737
|
+
await this.copyIfDifferent(
|
|
3738
|
+
assets.splashLogoDark,
|
|
3739
|
+
join25(splashDir, "logo_dark.png")
|
|
3740
|
+
);
|
|
3741
|
+
splashConfig += ` image_dark: "assets/splash/logo_dark.png"
|
|
3742
|
+
`;
|
|
3743
|
+
} else {
|
|
3744
|
+
splashConfig += ` image_dark: "assets/splash/logo.png"
|
|
3745
|
+
`;
|
|
3746
|
+
}
|
|
3747
|
+
if (assets.splashBackgroundDark) {
|
|
3748
|
+
await this.copyIfDifferent(
|
|
3749
|
+
assets.splashBackgroundDark,
|
|
3750
|
+
join25(splashDir, "background_dark.png")
|
|
3751
|
+
);
|
|
3752
|
+
splashConfig += ` background_image_dark: "assets/splash/background_dark.png"
|
|
3753
|
+
`;
|
|
3754
|
+
}
|
|
3755
|
+
splashConfig += `
|
|
3756
|
+
android_12:
|
|
3757
|
+
image: "assets/splash/logo.png"
|
|
3758
|
+
color: "${assets.splashBackgroundColor || "#FFFFFF"}"
|
|
3759
|
+
`;
|
|
3760
|
+
if (assets.splashBackground) {
|
|
3761
|
+
splashConfig += ` background_image: "assets/splash/background.png"
|
|
3762
|
+
`;
|
|
3763
|
+
}
|
|
3764
|
+
}
|
|
3765
|
+
devDeps.push("flutter_native_splash: ^2.3.10");
|
|
3766
|
+
pubspecAdditions += splashConfig;
|
|
3767
|
+
}
|
|
3768
|
+
if (devDeps.length > 0 || pubspecAdditions) {
|
|
3769
|
+
await this.updatePubspec(
|
|
3770
|
+
projectPath,
|
|
3771
|
+
devDeps,
|
|
3772
|
+
pubspecAdditions,
|
|
3773
|
+
logger2
|
|
3774
|
+
);
|
|
3775
|
+
}
|
|
3776
|
+
logger2.info("\u{1F4E6} \u8FD0\u884C flutter pub get...");
|
|
3777
|
+
await this.runCommand("flutter pub get", projectPath, logger2);
|
|
3778
|
+
if (assets.appIcon) {
|
|
3779
|
+
logger2.info("\u2699\uFE0F \u751F\u6210\u5E94\u7528\u56FE\u6807...");
|
|
3780
|
+
await this.runCommand(
|
|
3781
|
+
"dart run flutter_launcher_icons",
|
|
3782
|
+
projectPath,
|
|
3783
|
+
logger2
|
|
3784
|
+
);
|
|
3785
|
+
}
|
|
3786
|
+
if (assets.splashLogo) {
|
|
3787
|
+
logger2.info("\u2699\uFE0F \u751F\u6210\u542F\u52A8\u56FE...");
|
|
3788
|
+
await this.runCommand(
|
|
3789
|
+
"dart run flutter_native_splash:create",
|
|
3790
|
+
projectPath,
|
|
3791
|
+
logger2
|
|
3792
|
+
);
|
|
3793
|
+
}
|
|
3794
|
+
logger2.success("\u2705 \u5E94\u7528\u8D44\u6E90\u8BBE\u7F6E\u5B8C\u6210");
|
|
3795
|
+
return true;
|
|
3796
|
+
} catch (error) {
|
|
3797
|
+
logger2.error(`\u8BBE\u7F6E\u5E94\u7528\u8D44\u6E90\u5931\u8D25: ${error.message}`);
|
|
3798
|
+
return false;
|
|
3799
|
+
}
|
|
3800
|
+
}
|
|
3801
|
+
/**
|
|
3802
|
+
* 复制文件(当源路径与目标路径相同时跳过)
|
|
3803
|
+
*/
|
|
3804
|
+
async copyIfDifferent(srcPath, destPath) {
|
|
3805
|
+
const src = resolve2(srcPath);
|
|
3806
|
+
const dest = resolve2(destPath);
|
|
3807
|
+
if (src === dest) {
|
|
3808
|
+
return;
|
|
3809
|
+
}
|
|
3810
|
+
await fsx13.copy(srcPath, destPath, { overwrite: true });
|
|
3811
|
+
}
|
|
3812
|
+
/**
|
|
3813
|
+
* 更新 pubspec.yaml
|
|
3814
|
+
*/
|
|
3815
|
+
async updatePubspec(projectPath, devDeps, additions, logger2) {
|
|
3816
|
+
const pubspecPath = join25(projectPath, "pubspec.yaml");
|
|
3817
|
+
let content = readFileSync6(pubspecPath, "utf-8");
|
|
3818
|
+
if (devDeps.length > 0) {
|
|
3819
|
+
if (!content.includes("dev_dependencies:")) {
|
|
3820
|
+
content += "\ndev_dependencies:\n";
|
|
3821
|
+
}
|
|
3822
|
+
for (const dep of devDeps) {
|
|
3823
|
+
const depName = dep.split(":")[0].trim();
|
|
3824
|
+
if (!content.includes(depName)) {
|
|
3825
|
+
const devDepsMatch = content.match(/dev_dependencies:/);
|
|
3826
|
+
if (devDepsMatch && devDepsMatch.index !== void 0) {
|
|
3827
|
+
const insertPos = devDepsMatch.index + devDepsMatch[0].length;
|
|
3828
|
+
content = content.slice(0, insertPos) + `
|
|
3829
|
+
${dep}` + content.slice(insertPos);
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3834
|
+
content = this.removeExistingConfigBlocks(content, additions);
|
|
3835
|
+
content += "\n" + additions;
|
|
3836
|
+
writeFileSync10(pubspecPath, content, "utf-8");
|
|
3837
|
+
logger2.info("\u2713 pubspec.yaml \u5DF2\u66F4\u65B0");
|
|
3838
|
+
}
|
|
3839
|
+
/**
|
|
3840
|
+
* 移除已存在的配置块(如 flutter_launcher_icons:, flutter_native_splash:)
|
|
3841
|
+
*/
|
|
3842
|
+
removeExistingConfigBlocks(content, additions) {
|
|
3843
|
+
const configKeys = (additions.match(/^[a-z_]+:/gm) || []).map(
|
|
3844
|
+
(k) => k.replace(":", "").trim()
|
|
3845
|
+
);
|
|
3846
|
+
if (configKeys.length === 0) return content;
|
|
3847
|
+
const lines = content.split("\n");
|
|
3848
|
+
const resultLines = [];
|
|
3849
|
+
let skipBlock = false;
|
|
3850
|
+
for (const line of lines) {
|
|
3851
|
+
const topLevelMatch = line.match(/^([a-z_]+):.*$/);
|
|
3852
|
+
if (topLevelMatch) {
|
|
3853
|
+
const keyName = topLevelMatch[1];
|
|
3854
|
+
if (configKeys.includes(keyName)) {
|
|
3855
|
+
skipBlock = true;
|
|
3856
|
+
continue;
|
|
3857
|
+
} else {
|
|
3858
|
+
skipBlock = false;
|
|
3859
|
+
}
|
|
3860
|
+
}
|
|
3861
|
+
if (skipBlock && (line.trim() === "" || /^\s+/.test(line))) {
|
|
3862
|
+
continue;
|
|
3863
|
+
}
|
|
3864
|
+
if (skipBlock && line.trim() !== "" && !/^\s+/.test(line)) {
|
|
3865
|
+
skipBlock = false;
|
|
3866
|
+
}
|
|
3867
|
+
if (!skipBlock) {
|
|
3868
|
+
resultLines.push(line);
|
|
3869
|
+
}
|
|
3870
|
+
}
|
|
3871
|
+
return resultLines.join("\n");
|
|
3872
|
+
}
|
|
3873
|
+
/**
|
|
3874
|
+
* 执行命令
|
|
3875
|
+
*/
|
|
3876
|
+
async runCommand(cmd, cwd, logger2) {
|
|
3877
|
+
try {
|
|
3878
|
+
const { stdout, stderr } = await execPromise(cmd, {
|
|
3879
|
+
cwd,
|
|
3880
|
+
maxBuffer: 1024 * 1024 * 10
|
|
3881
|
+
});
|
|
3882
|
+
if (stdout) {
|
|
3883
|
+
const lines = stdout.split("\n").filter(
|
|
3884
|
+
(line) => line.includes("\u2713") || line.includes("success") || line.includes("completed")
|
|
3885
|
+
);
|
|
3886
|
+
if (lines.length > 0) {
|
|
3887
|
+
logger2.info(lines.join("\n"));
|
|
3888
|
+
}
|
|
3889
|
+
}
|
|
3890
|
+
if (stderr && !stderr.includes("Warning")) {
|
|
3891
|
+
logger2.warn(stderr);
|
|
3892
|
+
}
|
|
3893
|
+
} catch (error) {
|
|
3894
|
+
logger2.error(`\u547D\u4EE4\u6267\u884C\u5931\u8D25: ${cmd}`);
|
|
3895
|
+
if (error.stdout) logger2.info(error.stdout);
|
|
3896
|
+
if (error.stderr) logger2.error(error.stderr);
|
|
3897
|
+
throw error;
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3900
|
+
};
|
|
3901
|
+
|
|
3902
|
+
// src/utils/i18n_manager.ts
|
|
3903
|
+
import fsx14 from "fs-extra";
|
|
3904
|
+
import { join as join26, dirname as dirname5 } from "path";
|
|
3905
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3906
|
+
var I18nManager = class _I18nManager {
|
|
3907
|
+
static instance;
|
|
3908
|
+
locales = {};
|
|
3909
|
+
locale = "zh-CN";
|
|
3910
|
+
localesDir;
|
|
3911
|
+
constructor() {
|
|
3912
|
+
this.localesDir = this.resolveLocalesDir();
|
|
3913
|
+
this.loadLocales();
|
|
3914
|
+
const config = ConfigManager.getInstance();
|
|
3915
|
+
const envLang = typeof process !== "undefined" ? process.env.LANG || process.env.LANGUAGE || "" : "";
|
|
3916
|
+
this.locale = config.getLocale() || (envLang.startsWith("en") ? "en-US" : "zh-CN");
|
|
3917
|
+
}
|
|
3918
|
+
resolveLocalesDir() {
|
|
3919
|
+
const fallback = "locales";
|
|
3920
|
+
try {
|
|
3921
|
+
if (typeof import.meta !== "undefined" && import.meta.url) {
|
|
3922
|
+
const currentDir = dirname5(fileURLToPath2(import.meta.url));
|
|
3923
|
+
const result = join26(currentDir, "..", "locales");
|
|
3924
|
+
if (result && typeof result === "string") {
|
|
3925
|
+
return result;
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
} catch (e) {
|
|
3929
|
+
}
|
|
3930
|
+
try {
|
|
3931
|
+
if (typeof __dirname !== "undefined" && __dirname && typeof __dirname === "string") {
|
|
3932
|
+
const result = join26(__dirname, "..", "locales");
|
|
3933
|
+
if (result && typeof result === "string") {
|
|
3934
|
+
return result;
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
} catch (e) {
|
|
3938
|
+
}
|
|
3939
|
+
return fallback;
|
|
3940
|
+
}
|
|
3941
|
+
static getInstance() {
|
|
3942
|
+
if (!_I18nManager.instance) {
|
|
3943
|
+
_I18nManager.instance = new _I18nManager();
|
|
3944
|
+
}
|
|
3945
|
+
return _I18nManager.instance;
|
|
3946
|
+
}
|
|
3947
|
+
loadLocales() {
|
|
3948
|
+
try {
|
|
3949
|
+
if (!this.localesDir || typeof this.localesDir !== "string") {
|
|
3950
|
+
return;
|
|
3951
|
+
}
|
|
3952
|
+
const zhPath = join26(this.localesDir, "zh-CN.json");
|
|
3953
|
+
const enPath = join26(this.localesDir, "en-US.json");
|
|
3954
|
+
if (zhPath && typeof zhPath === "string" && fsx14.pathExistsSync(zhPath)) {
|
|
3955
|
+
this.locales["zh-CN"] = fsx14.readJsonSync(zhPath);
|
|
3956
|
+
}
|
|
3957
|
+
if (enPath && typeof enPath === "string" && fsx14.pathExistsSync(enPath)) {
|
|
3958
|
+
this.locales["en-US"] = fsx14.readJsonSync(enPath);
|
|
3959
|
+
}
|
|
3960
|
+
} catch (error) {
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
setLocale(locale) {
|
|
3964
|
+
if (this.locales[locale] || locale === "en-US" || locale === "zh-CN") {
|
|
3965
|
+
this.locale = locale;
|
|
3966
|
+
}
|
|
3967
|
+
}
|
|
3968
|
+
getLocale() {
|
|
3969
|
+
return this.locale;
|
|
3970
|
+
}
|
|
3971
|
+
t(key, params = {}) {
|
|
3972
|
+
let text = this.locales[this.locale]?.[key] || this.locales["en-US"]?.[key] || key;
|
|
3973
|
+
Object.keys(params).forEach((p) => {
|
|
3974
|
+
text = text.replace(new RegExp(`\\{${p}\\}`, "g"), String(params[p]));
|
|
3975
|
+
});
|
|
3976
|
+
return text;
|
|
3977
|
+
}
|
|
3978
|
+
};
|
|
3979
|
+
var t = (key, params) => I18nManager.getInstance().t(key, params);
|
|
3980
|
+
export {
|
|
3981
|
+
AppAssetsManager,
|
|
3982
|
+
BUILTIN_TEMPLATES,
|
|
3983
|
+
ConfigManager,
|
|
3984
|
+
ConsoleLogger,
|
|
3985
|
+
I18nManager,
|
|
3986
|
+
ProjectConfigManager,
|
|
3987
|
+
ProjectGenerator,
|
|
3988
|
+
TemplateGenerator,
|
|
3989
|
+
TemplateManager,
|
|
3990
|
+
checkFlutterInstalled,
|
|
3991
|
+
checkSnippetsVersion,
|
|
3992
|
+
cleanupTemplateFiles,
|
|
3993
|
+
copyCoreFiles,
|
|
3994
|
+
copyCustomTemplate,
|
|
3995
|
+
copyInfrastructure,
|
|
3996
|
+
copyNetworkFiles,
|
|
3997
|
+
copyTemplate,
|
|
3998
|
+
detectProjectTemplate,
|
|
3999
|
+
ensurePubspecName,
|
|
4000
|
+
generateComponent,
|
|
4001
|
+
generateIndexFile,
|
|
4002
|
+
generateModel,
|
|
4003
|
+
generateModule,
|
|
4004
|
+
generatePage,
|
|
4005
|
+
generateService,
|
|
4006
|
+
generateViewModel,
|
|
4007
|
+
generateWidget,
|
|
4008
|
+
getCoreFilesDir,
|
|
4009
|
+
getFlutterVersion,
|
|
4010
|
+
getModelPath,
|
|
4011
|
+
getNetworkDir,
|
|
4012
|
+
getPagePath,
|
|
4013
|
+
getRelativeImportPath,
|
|
4014
|
+
getServicePath,
|
|
4015
|
+
getSnippetContent,
|
|
4016
|
+
getStateManager,
|
|
4017
|
+
getTemplatesRootDir,
|
|
4018
|
+
getViewModelPath,
|
|
4019
|
+
getWidgetPath,
|
|
4020
|
+
injectNetworkExamples,
|
|
4021
|
+
isCustomTemplateProject,
|
|
4022
|
+
loadProjectSnippets,
|
|
4023
|
+
logger,
|
|
4024
|
+
removeTemplateSuffix,
|
|
4025
|
+
renderSnippet,
|
|
4026
|
+
replaceVariables,
|
|
4027
|
+
runFlutterCreate,
|
|
4028
|
+
runFlutterPubGet,
|
|
4029
|
+
t,
|
|
4030
|
+
toCamelCase,
|
|
4031
|
+
toKebabCase,
|
|
4032
|
+
toPascalCase,
|
|
4033
|
+
toSnakeCase,
|
|
4034
|
+
toTitleCase,
|
|
4035
|
+
updateIndexFile,
|
|
4036
|
+
upgradeSnippets
|
|
4037
|
+
};
|