flu-cli 2.0.5 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +17 -4
- package/config/dev.config.js +11 -11
- package/config/templates.js +10 -10
- package/index.js +554 -102
- package/lib/commands/add.js +365 -266
- package/lib/commands/assets.js +77 -78
- package/lib/commands/cache.js +29 -52
- package/lib/commands/completion.js +13 -11
- package/lib/commands/config.js +150 -44
- package/lib/commands/init-ai-base.js +89 -0
- package/lib/commands/newClack.js +269 -178
- package/lib/commands/snippets.js +58 -43
- package/lib/commands/template.js +98 -58
- package/lib/commands/templates.js +101 -57
- package/lib/commands/upload.js +313 -0
- package/lib/commands/vnext-options.js +206 -0
- package/lib/generators/model_generator.js +91 -88
- package/lib/generators/page_generator.js +100 -93
- package/lib/generators/service_generator.js +44 -39
- package/lib/generators/viewmodel_generator.js +25 -29
- package/lib/generators/widget_generator.js +30 -35
- package/lib/templates/templateCopier.js +14 -15
- package/lib/templates/templateManager.js +22 -21
- package/lib/utils/config.js +37 -20
- package/lib/utils/flutterHelper.js +2 -2
- package/lib/utils/i18n.js +3 -3
- package/lib/utils/index_updater.js +22 -23
- package/lib/utils/json-output.js +59 -0
- package/lib/utils/logger.js +17 -17
- package/lib/utils/project_detector.js +66 -66
- package/lib/utils/snippet_loader.js +21 -19
- package/lib/utils/string_helper.js +13 -13
- package/lib/utils/templateSelectorEnquirer.js +94 -108
- package/locales/en-US.json +1 -1
- package/locales/zh-CN.json +2 -2
- package/package.json +60 -57
- package/scripts/smoke-vnext-generate.mjs +1934 -0
- package/scripts/smoke-vnext-params.mjs +92 -0
- package/CLI.md +0 -513
- package/release.sh +0 -529
- package/scripts/e2e-state-tests.js +0 -116
- package/scripts/sync-base-to-templates.js +0 -108
- package/scripts/workspace-clone-all.sh +0 -101
- package/scripts/workspace-status-all.sh +0 -112
|
@@ -0,0 +1,1934 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import os from 'os'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { spawnSync } from 'child_process'
|
|
5
|
+
import { fileURLToPath } from 'url'
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
8
|
+
const __dirname = path.dirname(__filename)
|
|
9
|
+
const repoRoot = path.resolve(__dirname, '../../..')
|
|
10
|
+
const cliEntry = path.join(repoRoot, 'packages/cli/index.js')
|
|
11
|
+
const sourceTemplateRoots = {
|
|
12
|
+
lite: path.join(repoRoot, 'packages', 'core', 'templates', 'template_lite'),
|
|
13
|
+
modular: path.join(repoRoot, 'packages', 'core', 'templates', 'template_modular'),
|
|
14
|
+
clean: path.join(repoRoot, 'packages', 'core', 'templates', 'template_clean'),
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function assert(condition, message) {
|
|
18
|
+
if (!condition) {
|
|
19
|
+
throw new Error(message)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function runCli(args, cwd, extraEnv = {}) {
|
|
24
|
+
const result = spawnSync(process.execPath, [cliEntry, ...args], {
|
|
25
|
+
cwd,
|
|
26
|
+
env: {
|
|
27
|
+
...process.env,
|
|
28
|
+
FLU_CLI_NON_INTERACTIVE: '1',
|
|
29
|
+
...extraEnv,
|
|
30
|
+
},
|
|
31
|
+
encoding: 'utf8',
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
if (result.status !== 0) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
[
|
|
37
|
+
`CLI command failed: node ${path.relative(repoRoot, cliEntry)} ${args.join(' ')}`,
|
|
38
|
+
result.stdout,
|
|
39
|
+
result.stderr,
|
|
40
|
+
]
|
|
41
|
+
.filter(Boolean)
|
|
42
|
+
.join('\n'),
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return result
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function runCliJson(args, cwd, extraEnv = {}) {
|
|
50
|
+
const result = runCli([...args, '--json'], cwd, extraEnv)
|
|
51
|
+
const output = result.stdout.trim()
|
|
52
|
+
assert(output.startsWith('{'), `CLI JSON output should start with JSON object. actual=${output}`)
|
|
53
|
+
const parsed = JSON.parse(output)
|
|
54
|
+
assert(parsed.ok === true, `CLI JSON command should be ok: ${output}`)
|
|
55
|
+
return parsed
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function exists(target) {
|
|
59
|
+
return fs.existsSync(target)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function fileIncludes(target, needle) {
|
|
63
|
+
return fs.readFileSync(target, 'utf8').includes(needle)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function listDartFiles(dir) {
|
|
67
|
+
if (!fs.existsSync(dir)) return []
|
|
68
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
69
|
+
return entries.flatMap((entry) => {
|
|
70
|
+
const fullPath = path.join(dir, entry.name)
|
|
71
|
+
if (entry.isDirectory()) return listDartFiles(fullPath)
|
|
72
|
+
return entry.name.endsWith('.dart') ? [fullPath] : []
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readPubspecName(projectRoot) {
|
|
77
|
+
const pubspec = fs.readFileSync(path.join(projectRoot, 'pubspec.yaml'), 'utf8')
|
|
78
|
+
const match = pubspec.match(/^name:\s*([a-zA-Z0-9_]+)/m)
|
|
79
|
+
return match?.[1] || ''
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function assertProjectImportsResolvable(projectRoot) {
|
|
83
|
+
const packageName = readPubspecName(projectRoot)
|
|
84
|
+
assert(packageName, `pubspec name missing: ${projectRoot}`)
|
|
85
|
+
|
|
86
|
+
const errors = []
|
|
87
|
+
for (const file of listDartFiles(path.join(projectRoot, 'lib'))) {
|
|
88
|
+
const content = fs.readFileSync(file, 'utf8')
|
|
89
|
+
const matches = content.matchAll(/^(?:import|export) '([^']+)'/gm)
|
|
90
|
+
for (const match of matches) {
|
|
91
|
+
const specifier = match[1]
|
|
92
|
+
if (specifier.startsWith('.')) {
|
|
93
|
+
const target = path.normalize(path.join(path.dirname(file), specifier))
|
|
94
|
+
if (!fs.existsSync(target)) {
|
|
95
|
+
errors.push(`${path.relative(projectRoot, file)} -> ${specifier}`)
|
|
96
|
+
}
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const localPrefix = `package:${packageName}/`
|
|
101
|
+
if (specifier.startsWith(localPrefix)) {
|
|
102
|
+
const target = path.join(projectRoot, 'lib', specifier.slice(localPrefix.length))
|
|
103
|
+
if (!fs.existsSync(target)) {
|
|
104
|
+
errors.push(`${path.relative(projectRoot, file)} -> ${specifier}`)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
assert(errors.length === 0, `generated Dart imports should resolve:\n${errors.join('\n')}`)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function assertNoLegacyCoreMixinDirs(projectRoot, label) {
|
|
114
|
+
for (const dirName of ['page', 'service', 'viewmodel']) {
|
|
115
|
+
assert(
|
|
116
|
+
!exists(path.join(projectRoot, 'lib/core', dirName)),
|
|
117
|
+
`${label} should not generate legacy lib/core/${dirName} mixin directory`,
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function assertNoCoreMixinCandidates(projectRoot, label) {
|
|
123
|
+
assert(
|
|
124
|
+
!exists(path.join(projectRoot, 'lib/core/mixins')),
|
|
125
|
+
`${label} should not generate lib/core/mixins when mixin options are disabled`,
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function assertHomeHasNoStarterNoise(homePath, label) {
|
|
130
|
+
for (const needle of [
|
|
131
|
+
'项目已就绪',
|
|
132
|
+
'运营总览',
|
|
133
|
+
'模块总览',
|
|
134
|
+
'分层总览',
|
|
135
|
+
'交互事件',
|
|
136
|
+
'运行环境',
|
|
137
|
+
'协议状态',
|
|
138
|
+
'数据源',
|
|
139
|
+
'联系我们',
|
|
140
|
+
]) {
|
|
141
|
+
assert(!fileIncludes(homePath, needle), `${label} home should not include ${needle}`)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function assertHomeHasNoExampleModule(homePath, label) {
|
|
146
|
+
assert(
|
|
147
|
+
!fileIncludes(homePath, "_buildSectionTitle('示例')"),
|
|
148
|
+
`${label} home should not render examples section without selected examples`,
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function assertHomeHasExampleModule(homePath, label) {
|
|
153
|
+
assert(
|
|
154
|
+
fileIncludes(homePath, "_buildSectionTitle('示例')"),
|
|
155
|
+
`${label} home should render examples section when examples are selected`,
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function homePagePathForTemplate(projectRoot, template) {
|
|
160
|
+
if (template === 'modular') {
|
|
161
|
+
return path.join(projectRoot, 'lib/features/home/pages/home_page.dart')
|
|
162
|
+
}
|
|
163
|
+
if (template === 'clean') {
|
|
164
|
+
return path.join(projectRoot, 'lib/features/home/presentation/pages/home_page.dart')
|
|
165
|
+
}
|
|
166
|
+
return path.join(projectRoot, 'lib/pages/home_page.dart')
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function assertAddPageWorks(
|
|
170
|
+
projectRoot,
|
|
171
|
+
pageName,
|
|
172
|
+
expectedPagePath,
|
|
173
|
+
expectedViewModelPath,
|
|
174
|
+
cliArgs = [],
|
|
175
|
+
) {
|
|
176
|
+
runCli(['add', 'page', pageName, ...cliArgs], projectRoot)
|
|
177
|
+
|
|
178
|
+
assert(
|
|
179
|
+
exists(path.join(projectRoot, expectedPagePath)),
|
|
180
|
+
`add page should create ${expectedPagePath}`,
|
|
181
|
+
)
|
|
182
|
+
if (expectedViewModelPath) {
|
|
183
|
+
assert(
|
|
184
|
+
exists(path.join(projectRoot, expectedViewModelPath)),
|
|
185
|
+
`add page should create ${expectedViewModelPath}`,
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
assertProjectImportsResolvable(projectRoot)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function assertAddViewModelWorks(projectRoot, viewModelName, expectedViewModelPath) {
|
|
192
|
+
runCli(['add', 'vm', viewModelName], projectRoot)
|
|
193
|
+
|
|
194
|
+
assert(
|
|
195
|
+
exists(path.join(projectRoot, expectedViewModelPath)),
|
|
196
|
+
`add vm should create ${expectedViewModelPath}`,
|
|
197
|
+
)
|
|
198
|
+
assertProjectImportsResolvable(projectRoot)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function installFakeFlutter(root) {
|
|
202
|
+
const sdkRoot = path.join(root, 'fake_flutter')
|
|
203
|
+
const binDir = path.join(sdkRoot, 'bin')
|
|
204
|
+
const flutterBin = path.join(binDir, 'flutter')
|
|
205
|
+
|
|
206
|
+
fs.mkdirSync(binDir, { recursive: true })
|
|
207
|
+
fs.writeFileSync(
|
|
208
|
+
flutterBin,
|
|
209
|
+
[
|
|
210
|
+
'#!/bin/sh',
|
|
211
|
+
'set -eu',
|
|
212
|
+
'if [ "$#" -ge 1 ] && [ "$1" = "--version" ]; then',
|
|
213
|
+
' echo "Flutter 3.35.8 channel stable https://github.com/flutter/flutter.git"',
|
|
214
|
+
' echo "Tools Dart 3.9.2 DevTools 2.48.0"',
|
|
215
|
+
' exit 0',
|
|
216
|
+
'fi',
|
|
217
|
+
'if [ "$#" -ge 2 ] && [ "$1" = "create" ] && [ "$2" = "-h" ]; then',
|
|
218
|
+
' echo "Usage: flutter create [arguments] <output directory>"',
|
|
219
|
+
' echo " --platforms The platforms supported by this project. [android (default), ios (default), web, ohos]"',
|
|
220
|
+
' exit 0',
|
|
221
|
+
'fi',
|
|
222
|
+
'if [ "$#" -ge 2 ] && [ "$1" = "pub" ] && [ "$2" = "get" ]; then',
|
|
223
|
+
' echo "Resolving dependencies..."',
|
|
224
|
+
' echo "Got dependencies!"',
|
|
225
|
+
' exit 0',
|
|
226
|
+
'fi',
|
|
227
|
+
'if [ "$#" -ge 1 ] && [ "$1" = "create" ]; then',
|
|
228
|
+
' target=""',
|
|
229
|
+
' platforms="android,ios"',
|
|
230
|
+
' while [ "$#" -gt 0 ]; do',
|
|
231
|
+
' case "$1" in',
|
|
232
|
+
' --platforms)',
|
|
233
|
+
' shift',
|
|
234
|
+
' platforms="$1"',
|
|
235
|
+
' ;;',
|
|
236
|
+
' *)',
|
|
237
|
+
' target="$1"',
|
|
238
|
+
' ;;',
|
|
239
|
+
' esac',
|
|
240
|
+
' shift || true',
|
|
241
|
+
' done',
|
|
242
|
+
' mkdir -p "$target/lib" "$target/test"',
|
|
243
|
+
' printf "name: fake_project\\ndescription: Fake Flutter project for CLI smoke.\\npublish_to: none\\nenvironment:\\n sdk: \\"^3.0.0\\"\\ndependencies:\\n flutter:\\n sdk: flutter\\ndev_dependencies:\\n flutter_test:\\n sdk: flutter\\nflutter:\\n uses-material-design: true\\n" > "$target/pubspec.yaml"',
|
|
244
|
+
' cat > "$target/lib/main.dart" <<\'MAIN_EOF\'',
|
|
245
|
+
"import 'package:flutter/material.dart';",
|
|
246
|
+
'',
|
|
247
|
+
'void main() {',
|
|
248
|
+
' runApp(const MyApp());',
|
|
249
|
+
'}',
|
|
250
|
+
'',
|
|
251
|
+
'class MyApp extends StatelessWidget {',
|
|
252
|
+
' const MyApp({super.key});',
|
|
253
|
+
'',
|
|
254
|
+
' @override',
|
|
255
|
+
' Widget build(BuildContext context) {',
|
|
256
|
+
" return const MaterialApp(home: Scaffold(body: Center(child: Text('Fake'))));",
|
|
257
|
+
' }',
|
|
258
|
+
'}',
|
|
259
|
+
'MAIN_EOF',
|
|
260
|
+
' cat > "$target/test/widget_test.dart" <<\'TEST_EOF\'',
|
|
261
|
+
"import 'package:flutter/material.dart';",
|
|
262
|
+
"import 'package:flutter_test/flutter_test.dart';",
|
|
263
|
+
'',
|
|
264
|
+
'void main() {',
|
|
265
|
+
" testWidgets('Counter increments smoke test', (WidgetTester tester) async {",
|
|
266
|
+
' expect(1 + 1, 2);',
|
|
267
|
+
' });',
|
|
268
|
+
'}',
|
|
269
|
+
'TEST_EOF',
|
|
270
|
+
' oldIFS="$IFS"',
|
|
271
|
+
' IFS=","',
|
|
272
|
+
' for platform in $platforms; do',
|
|
273
|
+
' mkdir -p "$target/$platform"',
|
|
274
|
+
' if [ "$platform" = "android" ]; then',
|
|
275
|
+
' mkdir -p "$target/android/app/src/main"',
|
|
276
|
+
' mkdir -p "$target/android/app/src/debug"',
|
|
277
|
+
' printf "android { namespace \\"com.example.fake\\" defaultConfig { applicationId \\"com.example.fake\\" } }\\n" > "$target/android/app/build.gradle"',
|
|
278
|
+
' printf "<manifest package=\\"com.example.fake\\"></manifest>\\n" > "$target/android/app/src/main/AndroidManifest.xml"',
|
|
279
|
+
' fi',
|
|
280
|
+
' if [ "$platform" = "ios" ]; then',
|
|
281
|
+
' mkdir -p "$target/ios/Runner"',
|
|
282
|
+
' mkdir -p "$target/ios/Runner.xcodeproj"',
|
|
283
|
+
' printf "PRODUCT_BUNDLE_IDENTIFIER = com.example.fake;\\n" > "$target/ios/Runner.xcodeproj/project.pbxproj"',
|
|
284
|
+
' cat > "$target/ios/Podfile" <<\'PODFILE_EOF\'',
|
|
285
|
+
"platform :ios, '12.0'",
|
|
286
|
+
'',
|
|
287
|
+
"target 'Runner' do",
|
|
288
|
+
' use_frameworks!',
|
|
289
|
+
'end',
|
|
290
|
+
'',
|
|
291
|
+
'post_install do |installer|',
|
|
292
|
+
' installer.pods_project.targets.each do |target|',
|
|
293
|
+
' flutter_additional_ios_build_settings(target)',
|
|
294
|
+
' end',
|
|
295
|
+
'end',
|
|
296
|
+
'PODFILE_EOF',
|
|
297
|
+
' cat > "$target/ios/Runner/Info.plist" <<\'PLIST_EOF\'',
|
|
298
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
299
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
300
|
+
'<plist version="1.0">',
|
|
301
|
+
'<dict>',
|
|
302
|
+
' <key>CFBundleName</key>',
|
|
303
|
+
' <string>Fake</string>',
|
|
304
|
+
'</dict>',
|
|
305
|
+
'</plist>',
|
|
306
|
+
'PLIST_EOF',
|
|
307
|
+
' fi',
|
|
308
|
+
' if [ "$platform" = "ohos" ]; then',
|
|
309
|
+
' mkdir -p "$target/ohos/entry/src/main"',
|
|
310
|
+
' cat > "$target/ohos/entry/src/main/module.json5" <<\'OHOS_EOF\'',
|
|
311
|
+
'{',
|
|
312
|
+
' "module": {',
|
|
313
|
+
' "name": "entry",',
|
|
314
|
+
' "type": "entry",',
|
|
315
|
+
' "requestPermissions": [],',
|
|
316
|
+
' "abilities": [',
|
|
317
|
+
' {',
|
|
318
|
+
' "name": "EntryAbility"',
|
|
319
|
+
' }',
|
|
320
|
+
' ]',
|
|
321
|
+
' }',
|
|
322
|
+
'}',
|
|
323
|
+
'OHOS_EOF',
|
|
324
|
+
' fi',
|
|
325
|
+
' done',
|
|
326
|
+
' IFS="$oldIFS"',
|
|
327
|
+
' echo "Created project $target"',
|
|
328
|
+
' echo "All done!"',
|
|
329
|
+
' exit 0',
|
|
330
|
+
'fi',
|
|
331
|
+
'echo "fake flutter: unsupported command $*" >&2',
|
|
332
|
+
'exit 1',
|
|
333
|
+
'',
|
|
334
|
+
].join('\n'),
|
|
335
|
+
)
|
|
336
|
+
fs.chmodSync(flutterBin, 0o755)
|
|
337
|
+
|
|
338
|
+
return flutterBin
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'flu-cli-vnext-cli-smoke-'))
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
const rootHome = path.join(tmpRoot, 'home')
|
|
345
|
+
const fakeFlutterBin = installFakeFlutter(tmpRoot)
|
|
346
|
+
const cacheRoot = path.join(rootHome, '.flu-cli', 'templates')
|
|
347
|
+
fs.mkdirSync(cacheRoot, { recursive: true })
|
|
348
|
+
for (const [templateName, sourceTemplateRoot] of Object.entries(sourceTemplateRoots)) {
|
|
349
|
+
const cachedTemplateRoot = path.join(cacheRoot, `template_${templateName}`)
|
|
350
|
+
assert(exists(sourceTemplateRoot), `template cache missing: ${sourceTemplateRoot}`)
|
|
351
|
+
fs.cpSync(sourceTemplateRoot, cachedTemplateRoot, { recursive: true })
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
runCli(
|
|
355
|
+
[
|
|
356
|
+
'new',
|
|
357
|
+
'demo_vnext',
|
|
358
|
+
'--template',
|
|
359
|
+
'lite',
|
|
360
|
+
'--state',
|
|
361
|
+
'provider',
|
|
362
|
+
'--dir',
|
|
363
|
+
tmpRoot,
|
|
364
|
+
'--package',
|
|
365
|
+
'com.example.demo_vnext',
|
|
366
|
+
'--author',
|
|
367
|
+
'test',
|
|
368
|
+
'--auth',
|
|
369
|
+
'--helpers',
|
|
370
|
+
'payment,webview,permission,imagePicker',
|
|
371
|
+
'--examples',
|
|
372
|
+
'webview,permission,image-picker,payment',
|
|
373
|
+
'--flutter-sdk',
|
|
374
|
+
'custom',
|
|
375
|
+
'--flutter-bin',
|
|
376
|
+
fakeFlutterBin,
|
|
377
|
+
],
|
|
378
|
+
repoRoot,
|
|
379
|
+
{ HOME: rootHome },
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
const vnextRoot = path.join(tmpRoot, 'demo_vnext')
|
|
383
|
+
assert(exists(path.join(vnextRoot, 'lib/core/auth/auth_service.dart')), 'auth service missing')
|
|
384
|
+
assert(
|
|
385
|
+
exists(path.join(vnextRoot, 'lib/core/network/interceptors/auth_interceptor.dart')),
|
|
386
|
+
'auth interceptor missing',
|
|
387
|
+
)
|
|
388
|
+
assert(exists(path.join(vnextRoot, 'lib/helpers/payment/index.dart')), 'payment helper missing')
|
|
389
|
+
assert(exists(path.join(vnextRoot, 'lib/helpers/webview/index.dart')), 'webview helper missing')
|
|
390
|
+
assert(
|
|
391
|
+
exists(path.join(vnextRoot, 'lib/helpers/permission/index.dart')),
|
|
392
|
+
'permission helper missing',
|
|
393
|
+
)
|
|
394
|
+
assert(
|
|
395
|
+
exists(path.join(vnextRoot, 'lib/helpers/image_picker/index.dart')),
|
|
396
|
+
'image_picker helper missing',
|
|
397
|
+
)
|
|
398
|
+
assert(
|
|
399
|
+
fileIncludes(path.join(vnextRoot, 'lib/helpers/payment/index.dart'), 'PaymentExecutor'),
|
|
400
|
+
'payment helper contract missing',
|
|
401
|
+
)
|
|
402
|
+
assert(
|
|
403
|
+
fileIncludes(path.join(vnextRoot, 'lib/helpers/webview/index.dart'), 'WebViewController'),
|
|
404
|
+
'webview helper should include runnable webview_flutter implementation',
|
|
405
|
+
)
|
|
406
|
+
assert(
|
|
407
|
+
fileIncludes(
|
|
408
|
+
path.join(vnextRoot, 'lib/examples/webview_example_page.dart'),
|
|
409
|
+
'https://huozhiye.cn/flu-cli',
|
|
410
|
+
),
|
|
411
|
+
'webview example should open Flu CLI official docs by default',
|
|
412
|
+
)
|
|
413
|
+
assert(
|
|
414
|
+
fileIncludes(path.join(vnextRoot, 'lib/helpers/permission/index.dart'), 'permission_handler'),
|
|
415
|
+
'permission helper should include permission_handler implementation',
|
|
416
|
+
)
|
|
417
|
+
assert(
|
|
418
|
+
fileIncludes(
|
|
419
|
+
path.join(vnextRoot, 'lib/examples/permission_example_page.dart'),
|
|
420
|
+
'README.platform-permissions.md',
|
|
421
|
+
),
|
|
422
|
+
'permission example should point users to platform permission guide',
|
|
423
|
+
)
|
|
424
|
+
assert(
|
|
425
|
+
fileIncludes(path.join(vnextRoot, 'lib/examples/permission_example_page.dart'), '前往设置'),
|
|
426
|
+
'permission example should offer open-settings action for permanently denied status',
|
|
427
|
+
)
|
|
428
|
+
assert(
|
|
429
|
+
fileIncludes(
|
|
430
|
+
path.join(vnextRoot, 'lib/examples/permission_example_page.dart'),
|
|
431
|
+
'showDialog<bool>',
|
|
432
|
+
),
|
|
433
|
+
'permission example should show pre-permission rationale dialog before system request',
|
|
434
|
+
)
|
|
435
|
+
assert(
|
|
436
|
+
fileIncludes(path.join(vnextRoot, 'lib/examples/permission_example_page.dart'), '已授权'),
|
|
437
|
+
'permission example should render human-readable permission status labels',
|
|
438
|
+
)
|
|
439
|
+
assert(
|
|
440
|
+
fileIncludes(path.join(vnextRoot, 'lib/helpers/image_picker/index.dart'), 'ImagePickerHelper'),
|
|
441
|
+
'image picker helper should include runnable implementation',
|
|
442
|
+
)
|
|
443
|
+
assert(
|
|
444
|
+
fileIncludes(path.join(vnextRoot, 'lib/helpers/image_picker/index.dart'), 'readAsBytes'),
|
|
445
|
+
'image picker helper should capture bytes for in-page preview rendering',
|
|
446
|
+
)
|
|
447
|
+
assert(
|
|
448
|
+
fileIncludes(
|
|
449
|
+
path.join(vnextRoot, 'lib/examples/image_picker_example_page.dart'),
|
|
450
|
+
'Image.memory',
|
|
451
|
+
),
|
|
452
|
+
'image picker example should render selected image preview',
|
|
453
|
+
)
|
|
454
|
+
assert(
|
|
455
|
+
fileIncludes(
|
|
456
|
+
path.join(vnextRoot, 'lib/examples/image_picker_example_page.dart'),
|
|
457
|
+
'当前已选择图片',
|
|
458
|
+
),
|
|
459
|
+
'image picker example should show selected image summary card',
|
|
460
|
+
)
|
|
461
|
+
for (const file of [
|
|
462
|
+
'lib/examples/webview_example_page.dart',
|
|
463
|
+
'lib/examples/permission_example_page.dart',
|
|
464
|
+
'lib/examples/image_picker_example_page.dart',
|
|
465
|
+
'lib/examples/payment_shell_example_page.dart',
|
|
466
|
+
'lib/examples/index.dart',
|
|
467
|
+
]) {
|
|
468
|
+
assert(exists(path.join(vnextRoot, file)), `helper example file missing: ${file}`)
|
|
469
|
+
}
|
|
470
|
+
assert(
|
|
471
|
+
fileIncludes(
|
|
472
|
+
path.join(vnextRoot, 'lib/examples/image_picker_example_page.dart'),
|
|
473
|
+
"return '$bytes B';",
|
|
474
|
+
),
|
|
475
|
+
'image picker example should avoid unnecessary string interpolation braces',
|
|
476
|
+
)
|
|
477
|
+
assert(
|
|
478
|
+
fs.readFileSync(path.join(vnextRoot, 'lib/examples/index.dart'), 'utf8') ===
|
|
479
|
+
[
|
|
480
|
+
"export 'image_picker_example_page.dart';",
|
|
481
|
+
"export 'payment_shell_example_page.dart';",
|
|
482
|
+
"export 'permission_example_page.dart';",
|
|
483
|
+
"export 'webview_example_page.dart';",
|
|
484
|
+
'',
|
|
485
|
+
].join('\n'),
|
|
486
|
+
'helper examples index should keep exports sorted for strict directives_ordering',
|
|
487
|
+
)
|
|
488
|
+
assert(
|
|
489
|
+
fileIncludes(path.join(vnextRoot, 'lib/core/router/app_routes.dart'), 'webviewExample'),
|
|
490
|
+
'webview example route missing',
|
|
491
|
+
)
|
|
492
|
+
assert(
|
|
493
|
+
fileIncludes(path.join(vnextRoot, 'lib/core/router/app_routes.dart'), 'permissionExample'),
|
|
494
|
+
'permission example route missing',
|
|
495
|
+
)
|
|
496
|
+
assert(
|
|
497
|
+
fileIncludes(path.join(vnextRoot, 'lib/core/router/app_routes.dart'), 'imagePickerExample'),
|
|
498
|
+
'image picker example route missing',
|
|
499
|
+
)
|
|
500
|
+
assert(
|
|
501
|
+
fileIncludes(path.join(vnextRoot, 'lib/core/router/app_routes.dart'), 'paymentShellExample'),
|
|
502
|
+
'payment shell example route missing',
|
|
503
|
+
)
|
|
504
|
+
assert(
|
|
505
|
+
fileIncludes(path.join(vnextRoot, 'lib/pages/home_page.dart'), 'AppRoutes.webviewExample'),
|
|
506
|
+
'webview example home entry missing',
|
|
507
|
+
)
|
|
508
|
+
assertHomeHasNoStarterNoise(path.join(vnextRoot, 'lib/pages/home_page.dart'), 'helper examples')
|
|
509
|
+
assertHomeHasExampleModule(path.join(vnextRoot, 'lib/pages/home_page.dart'), 'helper examples')
|
|
510
|
+
assert(
|
|
511
|
+
fileIncludes(
|
|
512
|
+
path.join(vnextRoot, 'android/app/src/main/AndroidManifest.xml'),
|
|
513
|
+
'android.permission.CAMERA',
|
|
514
|
+
),
|
|
515
|
+
'Android camera permission missing',
|
|
516
|
+
)
|
|
517
|
+
assert(
|
|
518
|
+
fileIncludes(
|
|
519
|
+
path.join(vnextRoot, 'android/app/src/main/AndroidManifest.xml'),
|
|
520
|
+
'android.permission.READ_MEDIA_IMAGES',
|
|
521
|
+
),
|
|
522
|
+
'Android media image permission missing',
|
|
523
|
+
)
|
|
524
|
+
assert(
|
|
525
|
+
fileIncludes(path.join(vnextRoot, 'ios/Runner/Info.plist'), 'NSCameraUsageDescription'),
|
|
526
|
+
'iOS camera usage description missing',
|
|
527
|
+
)
|
|
528
|
+
assert(
|
|
529
|
+
fileIncludes(path.join(vnextRoot, 'ios/Runner/Info.plist'), 'NSPhotoLibraryUsageDescription'),
|
|
530
|
+
'iOS photo usage description missing',
|
|
531
|
+
)
|
|
532
|
+
assert(
|
|
533
|
+
fileIncludes(path.join(vnextRoot, 'ios/Podfile'), 'PERMISSION_CAMERA=1'),
|
|
534
|
+
'iOS Podfile camera macro missing',
|
|
535
|
+
)
|
|
536
|
+
assert(
|
|
537
|
+
fileIncludes(path.join(vnextRoot, 'ios/Podfile'), 'PERMISSION_PHOTOS=1'),
|
|
538
|
+
'iOS Podfile photos macro missing',
|
|
539
|
+
)
|
|
540
|
+
assert(
|
|
541
|
+
fileIncludes(
|
|
542
|
+
path.join(vnextRoot, 'ios/Podfile'),
|
|
543
|
+
'target.build_configurations.each do |config|',
|
|
544
|
+
),
|
|
545
|
+
'iOS Podfile macros should be wrapped in target build configurations loop',
|
|
546
|
+
)
|
|
547
|
+
assert(
|
|
548
|
+
exists(path.join(vnextRoot, 'lib/helpers/README.platform-permissions.md')),
|
|
549
|
+
'helper platform permission README missing',
|
|
550
|
+
)
|
|
551
|
+
assert(
|
|
552
|
+
fileIncludes(
|
|
553
|
+
path.join(vnextRoot, 'lib/helpers/README.platform-permissions.md'),
|
|
554
|
+
'ios/Runner/Info.plist',
|
|
555
|
+
),
|
|
556
|
+
'helper platform permission README should mention iOS permission path',
|
|
557
|
+
)
|
|
558
|
+
assert(
|
|
559
|
+
fileIncludes(path.join(vnextRoot, 'lib/helpers/README.platform-permissions.md'), 'ios/Podfile'),
|
|
560
|
+
'helper platform permission README should mention iOS Podfile macros',
|
|
561
|
+
)
|
|
562
|
+
assert(
|
|
563
|
+
!exists(path.join(vnextRoot, 'lib/pages/eg_list_page.dart')),
|
|
564
|
+
'network without networkGallery should not generate gallery list page',
|
|
565
|
+
)
|
|
566
|
+
assertNoCoreMixinCandidates(vnextRoot, 'default project')
|
|
567
|
+
assert(
|
|
568
|
+
exists(path.join(vnextRoot, 'lib/pages/splash_page.dart')),
|
|
569
|
+
'default starter project should generate SplashPage',
|
|
570
|
+
)
|
|
571
|
+
assert(
|
|
572
|
+
!exists(path.join(vnextRoot, 'lib/pages/user_list_page.dart')),
|
|
573
|
+
'default starter project should not keep legacy user list page',
|
|
574
|
+
)
|
|
575
|
+
assert(
|
|
576
|
+
!exists(path.join(vnextRoot, 'lib/viewmodels/user_list_viewmodel.dart')),
|
|
577
|
+
'default starter project should not keep legacy user list viewmodel',
|
|
578
|
+
)
|
|
579
|
+
assert(
|
|
580
|
+
!exists(path.join(vnextRoot, 'lib/widgets/user_item_widget.dart')),
|
|
581
|
+
'default starter project should not keep legacy user list widget',
|
|
582
|
+
)
|
|
583
|
+
assert(
|
|
584
|
+
!exists(path.join(vnextRoot, 'lib/models/mock_data.dart')),
|
|
585
|
+
'network without networkGallery should not generate gallery mock data',
|
|
586
|
+
)
|
|
587
|
+
assert(
|
|
588
|
+
!fileIncludes(path.join(vnextRoot, 'lib/core/router/app_routes.dart'), 'egList'),
|
|
589
|
+
'network without networkGallery should not register gallery route',
|
|
590
|
+
)
|
|
591
|
+
assert(
|
|
592
|
+
!exists(path.join(vnextRoot, 'lib/services/home_feed_service.dart')),
|
|
593
|
+
'network without networkGallery should not generate network-backed home feed service',
|
|
594
|
+
)
|
|
595
|
+
assert(
|
|
596
|
+
!fileIncludes(path.join(vnextRoot, 'lib/pages/home_page.dart'), 'AppRoutes.userList'),
|
|
597
|
+
'default home should not keep legacy user list entry',
|
|
598
|
+
)
|
|
599
|
+
assert(
|
|
600
|
+
!fileIncludes(path.join(vnextRoot, 'lib/core/router/app_routes.dart'), 'userList:'),
|
|
601
|
+
'default routes should not keep legacy user list route',
|
|
602
|
+
)
|
|
603
|
+
assert(
|
|
604
|
+
!fileIncludes(path.join(vnextRoot, 'lib/pages/index.dart'), 'user_list_page.dart'),
|
|
605
|
+
'default pages index should not export legacy user list page',
|
|
606
|
+
)
|
|
607
|
+
assert(
|
|
608
|
+
!fileIncludes(path.join(vnextRoot, 'lib/core/index.dart'), 'PaymentHelper'),
|
|
609
|
+
'helper export leaked into core index',
|
|
610
|
+
)
|
|
611
|
+
assert(
|
|
612
|
+
exists(path.join(vnextRoot, '.flu-cli.json')),
|
|
613
|
+
'built-in lite project should write .flu-cli.json',
|
|
614
|
+
)
|
|
615
|
+
assert(
|
|
616
|
+
fileIncludes(path.join(vnextRoot, '.flu-cli.json'), '"template": "lite"'),
|
|
617
|
+
'generated .flu-cli.json should preserve lite template metadata',
|
|
618
|
+
)
|
|
619
|
+
assert(
|
|
620
|
+
fileIncludes(path.join(vnextRoot, '.flu-cli.json'), '"mode": "mvvm"'),
|
|
621
|
+
'generated .flu-cli.json should persist architecture mode',
|
|
622
|
+
)
|
|
623
|
+
assertProjectImportsResolvable(vnextRoot)
|
|
624
|
+
|
|
625
|
+
runCli(
|
|
626
|
+
[
|
|
627
|
+
'new',
|
|
628
|
+
'demo_native_mixins',
|
|
629
|
+
'--template',
|
|
630
|
+
'lite',
|
|
631
|
+
'--state',
|
|
632
|
+
'provider',
|
|
633
|
+
'--dir',
|
|
634
|
+
tmpRoot,
|
|
635
|
+
'--package',
|
|
636
|
+
'com.example.demo_native_mixins',
|
|
637
|
+
'--author',
|
|
638
|
+
'test',
|
|
639
|
+
'--architecture-mode',
|
|
640
|
+
'native',
|
|
641
|
+
'--composition-profile',
|
|
642
|
+
'base_mixins',
|
|
643
|
+
'--enable-mixin-options',
|
|
644
|
+
'true',
|
|
645
|
+
'--flutter-sdk',
|
|
646
|
+
'custom',
|
|
647
|
+
'--flutter-bin',
|
|
648
|
+
fakeFlutterBin,
|
|
649
|
+
],
|
|
650
|
+
repoRoot,
|
|
651
|
+
{ HOME: rootHome },
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
const nativeMixinsRoot = path.join(tmpRoot, 'demo_native_mixins')
|
|
655
|
+
assert(
|
|
656
|
+
fileIncludes(path.join(nativeMixinsRoot, '.flu-cli.json'), '"mode": "native"'),
|
|
657
|
+
'native architecture mode should persist to .flu-cli.json',
|
|
658
|
+
)
|
|
659
|
+
assert(
|
|
660
|
+
fileIncludes(path.join(nativeMixinsRoot, '.flu-cli.json'), '"native": true'),
|
|
661
|
+
'native architecture should mark page/viewModel generators as native in .flu-cli.json',
|
|
662
|
+
)
|
|
663
|
+
assert(
|
|
664
|
+
fileIncludes(path.join(nativeMixinsRoot, '.flu-cli.json'), '"withBasePage": false'),
|
|
665
|
+
'native architecture should disable BasePage generation in .flu-cli.json',
|
|
666
|
+
)
|
|
667
|
+
assert(
|
|
668
|
+
fileIncludes(path.join(nativeMixinsRoot, '.flu-cli.json'), '"withViewModel": false'),
|
|
669
|
+
'native architecture should disable page ViewModel generation in .flu-cli.json',
|
|
670
|
+
)
|
|
671
|
+
assert(
|
|
672
|
+
fileIncludes(path.join(nativeMixinsRoot, '.flu-cli.json'), '"withBaseViewModel": false'),
|
|
673
|
+
'native architecture should disable BaseViewModel generation in .flu-cli.json',
|
|
674
|
+
)
|
|
675
|
+
assert(
|
|
676
|
+
!fileIncludes(path.join(nativeMixinsRoot, '.flu-cli.json'), '"mixinOptionDirs"'),
|
|
677
|
+
'native architecture should not keep mixin option directories in .flu-cli.json',
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
runCli(
|
|
681
|
+
[
|
|
682
|
+
'new',
|
|
683
|
+
'demo_base_mixins',
|
|
684
|
+
'--template',
|
|
685
|
+
'lite',
|
|
686
|
+
'--state',
|
|
687
|
+
'provider',
|
|
688
|
+
'--dir',
|
|
689
|
+
tmpRoot,
|
|
690
|
+
'--package',
|
|
691
|
+
'com.example.demo_base_mixins',
|
|
692
|
+
'--author',
|
|
693
|
+
'test',
|
|
694
|
+
'--network',
|
|
695
|
+
'--composition-profile',
|
|
696
|
+
'base_mixins',
|
|
697
|
+
'--enable-mixin-options',
|
|
698
|
+
'true',
|
|
699
|
+
'--flutter-sdk',
|
|
700
|
+
'custom',
|
|
701
|
+
'--flutter-bin',
|
|
702
|
+
fakeFlutterBin,
|
|
703
|
+
],
|
|
704
|
+
repoRoot,
|
|
705
|
+
{ HOME: rootHome },
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
const baseMixinsRoot = path.join(tmpRoot, 'demo_base_mixins')
|
|
709
|
+
assert(
|
|
710
|
+
exists(path.join(baseMixinsRoot, 'lib/core/mixins/page/keep_alive_mixin.dart')),
|
|
711
|
+
'base_mixins should generate page mixin candidates',
|
|
712
|
+
)
|
|
713
|
+
assert(
|
|
714
|
+
exists(path.join(baseMixinsRoot, 'lib/core/mixins/page/scroll_controller_mixin.dart')),
|
|
715
|
+
'base_mixins should generate page mixin utility candidates',
|
|
716
|
+
)
|
|
717
|
+
assert(
|
|
718
|
+
exists(path.join(baseMixinsRoot, 'lib/core/mixins/viewmodel/debounce_mixin.dart')),
|
|
719
|
+
'base_mixins should generate viewmodel mixin candidates',
|
|
720
|
+
)
|
|
721
|
+
assert(
|
|
722
|
+
exists(path.join(baseMixinsRoot, 'lib/core/mixins/service/request_guard_mixin.dart')),
|
|
723
|
+
'base_mixins should generate service mixin candidates',
|
|
724
|
+
)
|
|
725
|
+
assertNoLegacyCoreMixinDirs(baseMixinsRoot, 'base_mixins')
|
|
726
|
+
assert(
|
|
727
|
+
fileIncludes(path.join(baseMixinsRoot, '.flu-cli.json'), '"mixinOptionDirs"'),
|
|
728
|
+
'base_mixins should persist mixin option directories to .flu-cli.json',
|
|
729
|
+
)
|
|
730
|
+
assert(
|
|
731
|
+
fileIncludes(path.join(baseMixinsRoot, '.flu-cli.json'), '"withBasePage": true'),
|
|
732
|
+
'base_mixins should keep BasePage generation enabled',
|
|
733
|
+
)
|
|
734
|
+
assert(
|
|
735
|
+
exists(path.join(baseMixinsRoot, 'lib/core/base/base_service.dart')),
|
|
736
|
+
'base_mixins + network should generate BaseService',
|
|
737
|
+
)
|
|
738
|
+
assert(
|
|
739
|
+
fileIncludes(
|
|
740
|
+
path.join(baseMixinsRoot, 'lib/core/base/base_service.dart'),
|
|
741
|
+
'abstract class BaseService',
|
|
742
|
+
),
|
|
743
|
+
'base_mixins + network BaseService should be available for generated services',
|
|
744
|
+
)
|
|
745
|
+
assert(
|
|
746
|
+
fileIncludes(path.join(baseMixinsRoot, '.flu-cli.json'), '"baseServiceClass": "BaseService"'),
|
|
747
|
+
'base_mixins + network should persist BaseService for service generator',
|
|
748
|
+
)
|
|
749
|
+
runCli(
|
|
750
|
+
['add', 'page', 'mixin_probe', '--mixins', 'KeepAliveMixin,ScrollControllerMixin'],
|
|
751
|
+
baseMixinsRoot,
|
|
752
|
+
)
|
|
753
|
+
assert(
|
|
754
|
+
fileIncludes(
|
|
755
|
+
path.join(baseMixinsRoot, 'lib/pages/mixin_probe_page.dart'),
|
|
756
|
+
'with KeepAliveMixin, ScrollControllerMixin',
|
|
757
|
+
),
|
|
758
|
+
'base_mixins add page should apply selected mixins to generated page',
|
|
759
|
+
)
|
|
760
|
+
runCli(['add', 'vm', 'mixin_vm_probe', '--mixins', 'DebounceMixin'], baseMixinsRoot)
|
|
761
|
+
assert(
|
|
762
|
+
fileIncludes(
|
|
763
|
+
path.join(baseMixinsRoot, 'lib/viewmodels/mixin_vm_probe_viewmodel.dart'),
|
|
764
|
+
'extends BaseViewModel with DebounceMixin',
|
|
765
|
+
),
|
|
766
|
+
'base_mixins add vm should apply selected mixins to generated viewmodel',
|
|
767
|
+
)
|
|
768
|
+
runCli(['add', 'service', 'mixin_service_probe', '--mixins', 'RequestGuardMixin'], baseMixinsRoot)
|
|
769
|
+
assert(
|
|
770
|
+
fileIncludes(
|
|
771
|
+
path.join(baseMixinsRoot, 'lib/services/mixin_service_probe_service.dart'),
|
|
772
|
+
'class MixinServiceProbeService extends BaseService with RequestGuardMixin',
|
|
773
|
+
),
|
|
774
|
+
'base_mixins add service should extend BaseService and apply selected mixins',
|
|
775
|
+
)
|
|
776
|
+
assert(
|
|
777
|
+
fileIncludes(
|
|
778
|
+
path.join(baseMixinsRoot, 'lib/services/mixin_service_probe_service.dart'),
|
|
779
|
+
'MixinServiceProbeService({super.http});',
|
|
780
|
+
),
|
|
781
|
+
'base_mixins add service should reuse BaseService http injection',
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
runCli(
|
|
785
|
+
[
|
|
786
|
+
'new',
|
|
787
|
+
'demo_base_mixins_disabled',
|
|
788
|
+
'--template',
|
|
789
|
+
'lite',
|
|
790
|
+
'--state',
|
|
791
|
+
'provider',
|
|
792
|
+
'--dir',
|
|
793
|
+
tmpRoot,
|
|
794
|
+
'--package',
|
|
795
|
+
'com.example.demo_base_mixins_disabled',
|
|
796
|
+
'--author',
|
|
797
|
+
'test',
|
|
798
|
+
'--network',
|
|
799
|
+
'--composition-profile',
|
|
800
|
+
'base_mixins',
|
|
801
|
+
'--enable-mixin-options',
|
|
802
|
+
'false',
|
|
803
|
+
'--flutter-sdk',
|
|
804
|
+
'custom',
|
|
805
|
+
'--flutter-bin',
|
|
806
|
+
fakeFlutterBin,
|
|
807
|
+
],
|
|
808
|
+
repoRoot,
|
|
809
|
+
{ HOME: rootHome },
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
const baseMixinsDisabledRoot = path.join(tmpRoot, 'demo_base_mixins_disabled')
|
|
813
|
+
assertNoCoreMixinCandidates(baseMixinsDisabledRoot, 'base_mixins disabled')
|
|
814
|
+
assert(
|
|
815
|
+
!fileIncludes(path.join(baseMixinsDisabledRoot, '.flu-cli.json'), '"mixinOptionDirs"'),
|
|
816
|
+
'base_mixins disabled should not persist mixin option directories',
|
|
817
|
+
)
|
|
818
|
+
assert(
|
|
819
|
+
fileIncludes(path.join(baseMixinsDisabledRoot, '.flu-cli.json'), '"withBasePage": true'),
|
|
820
|
+
'base_mixins disabled should keep BasePage generation enabled',
|
|
821
|
+
)
|
|
822
|
+
assert(
|
|
823
|
+
exists(path.join(baseMixinsDisabledRoot, 'lib/core/base/base_service.dart')),
|
|
824
|
+
'base mixins disabled + network should still generate BaseService',
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
runCli(
|
|
828
|
+
[
|
|
829
|
+
'new',
|
|
830
|
+
'demo_pure_mixins',
|
|
831
|
+
'--template',
|
|
832
|
+
'lite',
|
|
833
|
+
'--state',
|
|
834
|
+
'provider',
|
|
835
|
+
'--dir',
|
|
836
|
+
tmpRoot,
|
|
837
|
+
'--package',
|
|
838
|
+
'com.example.demo_pure_mixins',
|
|
839
|
+
'--author',
|
|
840
|
+
'test',
|
|
841
|
+
'--network',
|
|
842
|
+
'--composition-profile',
|
|
843
|
+
'pure_mixins',
|
|
844
|
+
'--enable-mixin-options',
|
|
845
|
+
'true',
|
|
846
|
+
'--flutter-sdk',
|
|
847
|
+
'custom',
|
|
848
|
+
'--flutter-bin',
|
|
849
|
+
fakeFlutterBin,
|
|
850
|
+
],
|
|
851
|
+
repoRoot,
|
|
852
|
+
{ HOME: rootHome },
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
const pureMixinsRoot = path.join(tmpRoot, 'demo_pure_mixins')
|
|
856
|
+
assert(
|
|
857
|
+
!exists(path.join(pureMixinsRoot, 'lib/core/base/base_service.dart')),
|
|
858
|
+
'pure_mixins + network should not generate BaseService',
|
|
859
|
+
)
|
|
860
|
+
assert(
|
|
861
|
+
fileIncludes(path.join(pureMixinsRoot, '.flu-cli.json'), '"withBasePage": false'),
|
|
862
|
+
'pure_mixins should disable BasePage generation in .flu-cli.json',
|
|
863
|
+
)
|
|
864
|
+
assert(
|
|
865
|
+
fileIncludes(path.join(pureMixinsRoot, '.flu-cli.json'), '"withViewModel": true'),
|
|
866
|
+
'pure_mixins should keep page ViewModel generation enabled',
|
|
867
|
+
)
|
|
868
|
+
assert(
|
|
869
|
+
fileIncludes(path.join(pureMixinsRoot, '.flu-cli.json'), '"withBaseViewModel": false'),
|
|
870
|
+
'pure_mixins should disable BaseViewModel generation in .flu-cli.json',
|
|
871
|
+
)
|
|
872
|
+
assert(
|
|
873
|
+
exists(path.join(pureMixinsRoot, 'lib/core/mixins/page/keep_alive_mixin.dart')),
|
|
874
|
+
'pure_mixins should also generate page mixin candidates',
|
|
875
|
+
)
|
|
876
|
+
assertHomeHasNoStarterNoise(path.join(pureMixinsRoot, 'lib/pages/home_page.dart'), 'pure_mixins')
|
|
877
|
+
assertHomeHasNoExampleModule(path.join(pureMixinsRoot, 'lib/pages/home_page.dart'), 'pure_mixins')
|
|
878
|
+
assertNoLegacyCoreMixinDirs(pureMixinsRoot, 'pure_mixins')
|
|
879
|
+
assert(
|
|
880
|
+
fileIncludes(
|
|
881
|
+
path.join(pureMixinsRoot, 'lib/pages/home_page.dart'),
|
|
882
|
+
'class HomePage extends StatefulWidget',
|
|
883
|
+
),
|
|
884
|
+
'pure_mixins starter home page should no longer extend BasePage',
|
|
885
|
+
)
|
|
886
|
+
assert(
|
|
887
|
+
!fileIncludes(path.join(pureMixinsRoot, 'lib/pages/home_page.dart'), 'extends BasePage'),
|
|
888
|
+
'pure_mixins starter home page should not hardcode BasePage',
|
|
889
|
+
)
|
|
890
|
+
assert(
|
|
891
|
+
!exists(path.join(pureMixinsRoot, 'lib/core/base')),
|
|
892
|
+
'pure_mixins should not keep core/base directory',
|
|
893
|
+
)
|
|
894
|
+
assert(
|
|
895
|
+
fileIncludes(
|
|
896
|
+
path.join(pureMixinsRoot, 'lib/viewmodels/home_viewmodel.dart'),
|
|
897
|
+
'extends ChangeNotifier',
|
|
898
|
+
),
|
|
899
|
+
'pure_mixins starter HomeViewModel should not extend BaseViewModel',
|
|
900
|
+
)
|
|
901
|
+
assertAddPageWorks(
|
|
902
|
+
pureMixinsRoot,
|
|
903
|
+
'profile_check',
|
|
904
|
+
'lib/pages/profile_check_page.dart',
|
|
905
|
+
'lib/viewmodels/profile_check_viewmodel.dart',
|
|
906
|
+
)
|
|
907
|
+
assert(
|
|
908
|
+
fileIncludes(
|
|
909
|
+
path.join(pureMixinsRoot, 'lib/pages/profile_check_page.dart'),
|
|
910
|
+
'class ProfileCheckPage extends StatefulWidget',
|
|
911
|
+
),
|
|
912
|
+
'pure_mixins add page should generate a plain StatefulWidget page',
|
|
913
|
+
)
|
|
914
|
+
assert(
|
|
915
|
+
!fileIncludes(
|
|
916
|
+
path.join(pureMixinsRoot, 'lib/pages/profile_check_page.dart'),
|
|
917
|
+
'extends BasePage',
|
|
918
|
+
),
|
|
919
|
+
'pure_mixins add page should not extend BasePage',
|
|
920
|
+
)
|
|
921
|
+
assert(
|
|
922
|
+
fileIncludes(
|
|
923
|
+
path.join(pureMixinsRoot, 'lib/viewmodels/profile_check_viewmodel.dart'),
|
|
924
|
+
'extends ChangeNotifier',
|
|
925
|
+
),
|
|
926
|
+
'pure_mixins add page should generate a plain ChangeNotifier viewmodel',
|
|
927
|
+
)
|
|
928
|
+
assertAddViewModelWorks(pureMixinsRoot, 'vm_check', 'lib/viewmodels/vm_check_viewmodel.dart')
|
|
929
|
+
assert(
|
|
930
|
+
fileIncludes(
|
|
931
|
+
path.join(pureMixinsRoot, 'lib/viewmodels/vm_check_viewmodel.dart'),
|
|
932
|
+
'extends ChangeNotifier',
|
|
933
|
+
),
|
|
934
|
+
'pure_mixins add vm should generate a plain ChangeNotifier viewmodel',
|
|
935
|
+
)
|
|
936
|
+
runCli(
|
|
937
|
+
['add', 'page', 'pure_mixin_probe', '--mixins', 'KeepAliveMixin,ScrollControllerMixin'],
|
|
938
|
+
pureMixinsRoot,
|
|
939
|
+
)
|
|
940
|
+
assert(
|
|
941
|
+
fileIncludes(
|
|
942
|
+
path.join(pureMixinsRoot, 'lib/pages/pure_mixin_probe_page.dart'),
|
|
943
|
+
'with KeepAliveMixin, ScrollControllerMixin',
|
|
944
|
+
),
|
|
945
|
+
'pure_mixins add page should apply selected mixins to generated pure page',
|
|
946
|
+
)
|
|
947
|
+
runCli(['add', 'vm', 'pure_mixin_vm_probe', '--mixins', 'DebounceMixin'], pureMixinsRoot)
|
|
948
|
+
assert(
|
|
949
|
+
fileIncludes(
|
|
950
|
+
path.join(pureMixinsRoot, 'lib/viewmodels/pure_mixin_vm_probe_viewmodel.dart'),
|
|
951
|
+
'extends ChangeNotifier with DebounceMixin',
|
|
952
|
+
),
|
|
953
|
+
'pure_mixins add vm should apply selected mixins to generated pure viewmodel',
|
|
954
|
+
)
|
|
955
|
+
runCli(
|
|
956
|
+
['add', 'service', 'pure_mixin_service_probe', '--mixins', 'RequestGuardMixin'],
|
|
957
|
+
pureMixinsRoot,
|
|
958
|
+
)
|
|
959
|
+
assert(
|
|
960
|
+
fileIncludes(
|
|
961
|
+
path.join(pureMixinsRoot, 'lib/services/pure_mixin_service_probe_service.dart'),
|
|
962
|
+
'class PureMixinServiceProbeService with RequestGuardMixin',
|
|
963
|
+
),
|
|
964
|
+
'pure_mixins add service should apply selected mixins to generated pure service',
|
|
965
|
+
)
|
|
966
|
+
|
|
967
|
+
runCli(
|
|
968
|
+
[
|
|
969
|
+
'new',
|
|
970
|
+
'demo_pure_no_base',
|
|
971
|
+
'--template',
|
|
972
|
+
'lite',
|
|
973
|
+
'--state',
|
|
974
|
+
'provider',
|
|
975
|
+
'--dir',
|
|
976
|
+
tmpRoot,
|
|
977
|
+
'--package',
|
|
978
|
+
'com.example.demo_pure_no_base',
|
|
979
|
+
'--author',
|
|
980
|
+
'test',
|
|
981
|
+
'--composition-profile',
|
|
982
|
+
'pure',
|
|
983
|
+
'--flutter-sdk',
|
|
984
|
+
'custom',
|
|
985
|
+
'--flutter-bin',
|
|
986
|
+
fakeFlutterBin,
|
|
987
|
+
],
|
|
988
|
+
repoRoot,
|
|
989
|
+
{ HOME: rootHome },
|
|
990
|
+
)
|
|
991
|
+
|
|
992
|
+
const pureRoot = path.join(tmpRoot, 'demo_pure_no_base')
|
|
993
|
+
assert(
|
|
994
|
+
fileIncludes(path.join(pureRoot, '.flu-cli.json'), '"withBasePage": false'),
|
|
995
|
+
'pure should disable BasePage generation in .flu-cli.json',
|
|
996
|
+
)
|
|
997
|
+
assert(
|
|
998
|
+
fileIncludes(path.join(pureRoot, '.flu-cli.json'), '"withViewModel": true'),
|
|
999
|
+
'pure should keep page ViewModel generation enabled',
|
|
1000
|
+
)
|
|
1001
|
+
assert(
|
|
1002
|
+
fileIncludes(
|
|
1003
|
+
path.join(pureRoot, 'lib/pages/home_page.dart'),
|
|
1004
|
+
'class HomePage extends StatefulWidget',
|
|
1005
|
+
),
|
|
1006
|
+
'pure starter home page should use a plain StatefulWidget',
|
|
1007
|
+
)
|
|
1008
|
+
assert(
|
|
1009
|
+
!fileIncludes(path.join(pureRoot, 'lib/pages/home_page.dart'), 'extends BasePage'),
|
|
1010
|
+
'pure starter home page should not hardcode BasePage',
|
|
1011
|
+
)
|
|
1012
|
+
assert(!exists(path.join(pureRoot, 'lib/core/base')), 'pure should not keep core/base directory')
|
|
1013
|
+
assertNoCoreMixinCandidates(pureRoot, 'pure')
|
|
1014
|
+
|
|
1015
|
+
runCli(
|
|
1016
|
+
[
|
|
1017
|
+
'new',
|
|
1018
|
+
'demo_base_profile',
|
|
1019
|
+
'--template',
|
|
1020
|
+
'lite',
|
|
1021
|
+
'--state',
|
|
1022
|
+
'provider',
|
|
1023
|
+
'--dir',
|
|
1024
|
+
tmpRoot,
|
|
1025
|
+
'--package',
|
|
1026
|
+
'com.example.demo_base_profile',
|
|
1027
|
+
'--author',
|
|
1028
|
+
'test',
|
|
1029
|
+
'--network',
|
|
1030
|
+
'--composition-profile',
|
|
1031
|
+
'base',
|
|
1032
|
+
'--flutter-sdk',
|
|
1033
|
+
'custom',
|
|
1034
|
+
'--flutter-bin',
|
|
1035
|
+
fakeFlutterBin,
|
|
1036
|
+
],
|
|
1037
|
+
repoRoot,
|
|
1038
|
+
{ HOME: rootHome },
|
|
1039
|
+
)
|
|
1040
|
+
|
|
1041
|
+
const baseProfileRoot = path.join(tmpRoot, 'demo_base_profile')
|
|
1042
|
+
assertAddPageWorks(
|
|
1043
|
+
baseProfileRoot,
|
|
1044
|
+
'profile_check',
|
|
1045
|
+
'lib/pages/profile_check_page.dart',
|
|
1046
|
+
'lib/viewmodels/profile_check_viewmodel.dart',
|
|
1047
|
+
)
|
|
1048
|
+
assert(
|
|
1049
|
+
fileIncludes(
|
|
1050
|
+
path.join(baseProfileRoot, 'lib/pages/profile_check_page.dart'),
|
|
1051
|
+
'class ProfileCheckPage extends BasePage<ProfileCheckViewModel>',
|
|
1052
|
+
),
|
|
1053
|
+
'base profile add page should continue extending BasePage',
|
|
1054
|
+
)
|
|
1055
|
+
assert(
|
|
1056
|
+
fileIncludes(
|
|
1057
|
+
path.join(baseProfileRoot, 'lib/viewmodels/profile_check_viewmodel.dart'),
|
|
1058
|
+
'extends BaseViewModel',
|
|
1059
|
+
),
|
|
1060
|
+
'base profile add page should continue generating BaseViewModel',
|
|
1061
|
+
)
|
|
1062
|
+
assertAddViewModelWorks(baseProfileRoot, 'vm_check', 'lib/viewmodels/vm_check_viewmodel.dart')
|
|
1063
|
+
assert(
|
|
1064
|
+
fileIncludes(
|
|
1065
|
+
path.join(baseProfileRoot, 'lib/viewmodels/vm_check_viewmodel.dart'),
|
|
1066
|
+
'extends BaseViewModel',
|
|
1067
|
+
),
|
|
1068
|
+
'base profile add vm should continue extending BaseViewModel',
|
|
1069
|
+
)
|
|
1070
|
+
assert(
|
|
1071
|
+
exists(path.join(baseProfileRoot, 'lib/core/base/base_service.dart')),
|
|
1072
|
+
'base + network should generate BaseService',
|
|
1073
|
+
)
|
|
1074
|
+
runCli(['add', 'service', 'profile_data'], baseProfileRoot)
|
|
1075
|
+
assert(
|
|
1076
|
+
fileIncludes(
|
|
1077
|
+
path.join(baseProfileRoot, 'lib/services/profile_data_service.dart'),
|
|
1078
|
+
'class ProfileDataService extends BaseService',
|
|
1079
|
+
),
|
|
1080
|
+
'base + network add service should extend BaseService',
|
|
1081
|
+
)
|
|
1082
|
+
assert(
|
|
1083
|
+
fileIncludes(
|
|
1084
|
+
path.join(baseProfileRoot, 'lib/services/profile_data_service.dart'),
|
|
1085
|
+
'ProfileDataService({super.http});',
|
|
1086
|
+
),
|
|
1087
|
+
'base + network add service should reuse BaseService http injection',
|
|
1088
|
+
)
|
|
1089
|
+
|
|
1090
|
+
runCli(
|
|
1091
|
+
[
|
|
1092
|
+
'new',
|
|
1093
|
+
'demo_webview_only',
|
|
1094
|
+
'--template',
|
|
1095
|
+
'lite',
|
|
1096
|
+
'--state',
|
|
1097
|
+
'provider',
|
|
1098
|
+
'--dir',
|
|
1099
|
+
tmpRoot,
|
|
1100
|
+
'--package',
|
|
1101
|
+
'com.example.demo_webview_only',
|
|
1102
|
+
'--author',
|
|
1103
|
+
'test',
|
|
1104
|
+
'--helpers',
|
|
1105
|
+
'webview',
|
|
1106
|
+
'--examples',
|
|
1107
|
+
'webview',
|
|
1108
|
+
'--flutter-sdk',
|
|
1109
|
+
'custom',
|
|
1110
|
+
'--flutter-bin',
|
|
1111
|
+
fakeFlutterBin,
|
|
1112
|
+
],
|
|
1113
|
+
repoRoot,
|
|
1114
|
+
{ HOME: rootHome },
|
|
1115
|
+
)
|
|
1116
|
+
|
|
1117
|
+
const webviewOnlyRoot = path.join(tmpRoot, 'demo_webview_only')
|
|
1118
|
+
assert(
|
|
1119
|
+
exists(path.join(webviewOnlyRoot, 'lib/examples/webview_example_page.dart')),
|
|
1120
|
+
'webview-only example page missing',
|
|
1121
|
+
)
|
|
1122
|
+
assert(
|
|
1123
|
+
!exists(path.join(webviewOnlyRoot, 'lib/examples/permission_example_page.dart')),
|
|
1124
|
+
'webview-only project should not generate permission example page',
|
|
1125
|
+
)
|
|
1126
|
+
assert(
|
|
1127
|
+
fileIncludes(
|
|
1128
|
+
path.join(webviewOnlyRoot, 'lib/examples/index.dart'),
|
|
1129
|
+
'webview_example_page.dart',
|
|
1130
|
+
),
|
|
1131
|
+
'webview-only examples index should export webview page',
|
|
1132
|
+
)
|
|
1133
|
+
assert(
|
|
1134
|
+
!fileIncludes(
|
|
1135
|
+
path.join(webviewOnlyRoot, 'lib/examples/index.dart'),
|
|
1136
|
+
'permission_example_page.dart',
|
|
1137
|
+
),
|
|
1138
|
+
'webview-only examples index should not export missing permission page',
|
|
1139
|
+
)
|
|
1140
|
+
assertHomeHasNoStarterNoise(
|
|
1141
|
+
path.join(webviewOnlyRoot, 'lib/pages/home_page.dart'),
|
|
1142
|
+
'webview-only',
|
|
1143
|
+
)
|
|
1144
|
+
assertHomeHasExampleModule(path.join(webviewOnlyRoot, 'lib/pages/home_page.dart'), 'webview-only')
|
|
1145
|
+
assertProjectImportsResolvable(webviewOnlyRoot)
|
|
1146
|
+
|
|
1147
|
+
runCli(
|
|
1148
|
+
[
|
|
1149
|
+
'new',
|
|
1150
|
+
'demo_network_gallery',
|
|
1151
|
+
'--template',
|
|
1152
|
+
'lite',
|
|
1153
|
+
'--state',
|
|
1154
|
+
'provider',
|
|
1155
|
+
'--dir',
|
|
1156
|
+
tmpRoot,
|
|
1157
|
+
'--package',
|
|
1158
|
+
'com.example.demo_network_gallery',
|
|
1159
|
+
'--author',
|
|
1160
|
+
'test',
|
|
1161
|
+
'--examples',
|
|
1162
|
+
'networkGallery',
|
|
1163
|
+
'--flutter-sdk',
|
|
1164
|
+
'custom',
|
|
1165
|
+
'--flutter-bin',
|
|
1166
|
+
fakeFlutterBin,
|
|
1167
|
+
],
|
|
1168
|
+
repoRoot,
|
|
1169
|
+
{ HOME: rootHome },
|
|
1170
|
+
)
|
|
1171
|
+
|
|
1172
|
+
const networkGalleryRoot = path.join(tmpRoot, 'demo_network_gallery')
|
|
1173
|
+
assert(
|
|
1174
|
+
exists(path.join(networkGalleryRoot, 'lib/pages/eg_list_page.dart')),
|
|
1175
|
+
'networkGallery should generate gallery list page',
|
|
1176
|
+
)
|
|
1177
|
+
assert(
|
|
1178
|
+
exists(path.join(networkGalleryRoot, 'lib/services/eg_service.dart')),
|
|
1179
|
+
'networkGallery should generate gallery service',
|
|
1180
|
+
)
|
|
1181
|
+
assert(
|
|
1182
|
+
exists(path.join(networkGalleryRoot, 'lib/models/mock_data.dart')),
|
|
1183
|
+
'networkGallery should generate default mock data',
|
|
1184
|
+
)
|
|
1185
|
+
assert(
|
|
1186
|
+
fileIncludes(path.join(networkGalleryRoot, 'lib/models/mock_data.dart'), 'feedList'),
|
|
1187
|
+
'networkGallery mock data should contain feedList',
|
|
1188
|
+
)
|
|
1189
|
+
assert(
|
|
1190
|
+
fileIncludes(path.join(networkGalleryRoot, 'lib/services/eg_service.dart'), '/feed-app'),
|
|
1191
|
+
'networkGallery should keep gallery request path inside example service',
|
|
1192
|
+
)
|
|
1193
|
+
assert(
|
|
1194
|
+
fileIncludes(
|
|
1195
|
+
path.join(networkGalleryRoot, 'lib/services/eg_service.dart'),
|
|
1196
|
+
'TuChongExampleAdapter',
|
|
1197
|
+
),
|
|
1198
|
+
'networkGallery should keep gallery response adapter inside example service',
|
|
1199
|
+
)
|
|
1200
|
+
assert(
|
|
1201
|
+
!fileIncludes(
|
|
1202
|
+
path.join(networkGalleryRoot, 'lib/core/network/response_adapter.dart'),
|
|
1203
|
+
'TuChongAdapter',
|
|
1204
|
+
),
|
|
1205
|
+
'network core should not contain gallery-specific TuChong adapter',
|
|
1206
|
+
)
|
|
1207
|
+
assert(
|
|
1208
|
+
fileIncludes(path.join(networkGalleryRoot, 'lib/pages/eg_list_page.dart'), 'EgListPage'),
|
|
1209
|
+
'networkGallery should generate EgListPage',
|
|
1210
|
+
)
|
|
1211
|
+
assert(
|
|
1212
|
+
fileIncludes(path.join(networkGalleryRoot, 'lib/core/router/app_routes.dart'), 'egList'),
|
|
1213
|
+
'networkGallery should register egList route',
|
|
1214
|
+
)
|
|
1215
|
+
const galleryRoutes = fs.readFileSync(
|
|
1216
|
+
path.join(networkGalleryRoot, 'lib/core/router/app_routes.dart'),
|
|
1217
|
+
'utf8',
|
|
1218
|
+
)
|
|
1219
|
+
const egListMappingCount = (galleryRoutes.match(/egList:\s*\(context\)\s*=>/g) || []).length
|
|
1220
|
+
assert(egListMappingCount === 1, 'networkGallery should register egList route only once')
|
|
1221
|
+
assert(
|
|
1222
|
+
fileIncludes(path.join(networkGalleryRoot, 'lib/pages/home_page.dart'), 'AppRoutes.egList'),
|
|
1223
|
+
'networkGallery should add homepage gallery entry',
|
|
1224
|
+
)
|
|
1225
|
+
assertHomeHasNoStarterNoise(
|
|
1226
|
+
path.join(networkGalleryRoot, 'lib/pages/home_page.dart'),
|
|
1227
|
+
'networkGallery',
|
|
1228
|
+
)
|
|
1229
|
+
assertHomeHasExampleModule(
|
|
1230
|
+
path.join(networkGalleryRoot, 'lib/pages/home_page.dart'),
|
|
1231
|
+
'networkGallery',
|
|
1232
|
+
)
|
|
1233
|
+
const analysisOptions = fs.readFileSync(
|
|
1234
|
+
path.join(networkGalleryRoot, 'analysis_options.yaml'),
|
|
1235
|
+
'utf8',
|
|
1236
|
+
)
|
|
1237
|
+
assert(
|
|
1238
|
+
analysisOptions.includes('prefer_single_quotes: true'),
|
|
1239
|
+
'generated project should apply Flu strict analysis options',
|
|
1240
|
+
)
|
|
1241
|
+
assert(
|
|
1242
|
+
analysisOptions.includes('Flu CLI 生成的项目级 Dart/Flutter 静态检查配置'),
|
|
1243
|
+
'generated analysis_options should explain Flu lint intent',
|
|
1244
|
+
)
|
|
1245
|
+
assert(
|
|
1246
|
+
!analysisOptions.includes('Uncomment to disable'),
|
|
1247
|
+
'generated analysis_options should not keep Flutter default comments',
|
|
1248
|
+
)
|
|
1249
|
+
const generatedReadme = fs.readFileSync(path.join(networkGalleryRoot, 'README.md'), 'utf8')
|
|
1250
|
+
const generatedGuide = fs.readFileSync(
|
|
1251
|
+
path.join(networkGalleryRoot, 'DEVELOPER_GUIDE.md'),
|
|
1252
|
+
'utf8',
|
|
1253
|
+
)
|
|
1254
|
+
const generatedEnvDev = fs.readFileSync(path.join(networkGalleryRoot, '.env.dev'), 'utf8')
|
|
1255
|
+
const generatedEnvStaging = fs.readFileSync(path.join(networkGalleryRoot, '.env.staging'), 'utf8')
|
|
1256
|
+
const generatedEnvProdExample = fs.readFileSync(
|
|
1257
|
+
path.join(networkGalleryRoot, '.env.prod.example'),
|
|
1258
|
+
'utf8',
|
|
1259
|
+
)
|
|
1260
|
+
assert(
|
|
1261
|
+
generatedReadme.includes('当前项目配置'),
|
|
1262
|
+
'generated README should focus on current project configuration',
|
|
1263
|
+
)
|
|
1264
|
+
assert(generatedReadme.includes('首页样板'), 'generated README should explain home sample slot')
|
|
1265
|
+
assert(
|
|
1266
|
+
generatedGuide.includes('项目升级建议'),
|
|
1267
|
+
'generated developer guide should explain upgrade path',
|
|
1268
|
+
)
|
|
1269
|
+
assert(
|
|
1270
|
+
generatedGuide.includes('DEMO_LIST_MODE'),
|
|
1271
|
+
'generated developer guide should explain demo list mode',
|
|
1272
|
+
)
|
|
1273
|
+
assert(
|
|
1274
|
+
generatedEnvDev.includes('DEMO_LIST_MODE=finite'),
|
|
1275
|
+
'generated .env.dev should include finite demo list mode',
|
|
1276
|
+
)
|
|
1277
|
+
assert(
|
|
1278
|
+
fileIncludes(
|
|
1279
|
+
path.join(networkGalleryRoot, 'lib/core/config/app_env.dart'),
|
|
1280
|
+
'defaultValue: true',
|
|
1281
|
+
),
|
|
1282
|
+
'generated AppEnv should default USE_MOCK_DATA to true for test/dev safety',
|
|
1283
|
+
)
|
|
1284
|
+
assert(
|
|
1285
|
+
generatedEnvStaging.includes('DEMO_LIST_MODE=load_more_error'),
|
|
1286
|
+
'generated .env.staging should include load-more error mode',
|
|
1287
|
+
)
|
|
1288
|
+
assert(
|
|
1289
|
+
generatedEnvProdExample.includes('DEMO_LIST_MODE=standard'),
|
|
1290
|
+
'generated .env.prod.example should include standard mode',
|
|
1291
|
+
)
|
|
1292
|
+
assert(
|
|
1293
|
+
!fileIncludes(path.join(networkGalleryRoot, 'lib/main.dart'), 'kIsWeb'),
|
|
1294
|
+
'generated main.dart should not force mock data by platform',
|
|
1295
|
+
)
|
|
1296
|
+
assert(
|
|
1297
|
+
exists(path.join(networkGalleryRoot, 'lib/viewmodels/user_list_viewmodel.dart')),
|
|
1298
|
+
'networkGallery starter project should generate user list viewmodel for default list slot',
|
|
1299
|
+
)
|
|
1300
|
+
assert(
|
|
1301
|
+
fileIncludes(
|
|
1302
|
+
path.join(networkGalleryRoot, 'lib/viewmodels/user_list_viewmodel.dart'),
|
|
1303
|
+
'EnvConfig.demoListMode',
|
|
1304
|
+
),
|
|
1305
|
+
'generated list sample should read DEMO_LIST_MODE from EnvConfig',
|
|
1306
|
+
)
|
|
1307
|
+
assert(
|
|
1308
|
+
exists(path.join(networkGalleryRoot, 'lib/services/home_feed_service.dart')),
|
|
1309
|
+
'networkGallery should generate home feed service for default list slot',
|
|
1310
|
+
)
|
|
1311
|
+
assert(
|
|
1312
|
+
exists(path.join(networkGalleryRoot, 'lib/pages/splash_page.dart')),
|
|
1313
|
+
'networkGallery starter project should generate SplashPage',
|
|
1314
|
+
)
|
|
1315
|
+
assert(
|
|
1316
|
+
exists(path.join(networkGalleryRoot, 'lib/pages/user_list_page.dart')),
|
|
1317
|
+
'networkGallery starter project should keep user list page for list slot demo',
|
|
1318
|
+
)
|
|
1319
|
+
assert(
|
|
1320
|
+
fileIncludes(path.join(networkGalleryRoot, 'lib/services/home_feed_service.dart'), '/posts'),
|
|
1321
|
+
'home feed service should use runnable /posts network sample',
|
|
1322
|
+
)
|
|
1323
|
+
assert(
|
|
1324
|
+
fileIncludes(
|
|
1325
|
+
path.join(networkGalleryRoot, 'lib/viewmodels/user_list_viewmodel.dart'),
|
|
1326
|
+
'HomeFeedService',
|
|
1327
|
+
),
|
|
1328
|
+
'networkGallery list sample should use HomeFeedService',
|
|
1329
|
+
)
|
|
1330
|
+
assert(
|
|
1331
|
+
!fileIncludes(path.join(networkGalleryRoot, 'lib/pages/home_page.dart'), 'AppRoutes.userList'),
|
|
1332
|
+
'networkGallery home should not keep legacy user list entry',
|
|
1333
|
+
)
|
|
1334
|
+
assert(
|
|
1335
|
+
!fileIncludes(path.join(networkGalleryRoot, 'lib/core/router/app_routes.dart'), 'userList:'),
|
|
1336
|
+
'networkGallery routes should not keep legacy user list route',
|
|
1337
|
+
)
|
|
1338
|
+
assertProjectImportsResolvable(networkGalleryRoot)
|
|
1339
|
+
|
|
1340
|
+
runCli(
|
|
1341
|
+
[
|
|
1342
|
+
'new',
|
|
1343
|
+
'demo_harmony',
|
|
1344
|
+
'--template',
|
|
1345
|
+
'lite',
|
|
1346
|
+
'--state',
|
|
1347
|
+
'provider',
|
|
1348
|
+
'--dir',
|
|
1349
|
+
tmpRoot,
|
|
1350
|
+
'--package',
|
|
1351
|
+
'com.example.demo_harmony',
|
|
1352
|
+
'--author',
|
|
1353
|
+
'test',
|
|
1354
|
+
'--no-network',
|
|
1355
|
+
'--platforms',
|
|
1356
|
+
'harmony',
|
|
1357
|
+
'--helpers',
|
|
1358
|
+
'webview,permission,imagePicker',
|
|
1359
|
+
'--flutter-sdk',
|
|
1360
|
+
'custom',
|
|
1361
|
+
'--flutter-bin',
|
|
1362
|
+
fakeFlutterBin,
|
|
1363
|
+
],
|
|
1364
|
+
repoRoot,
|
|
1365
|
+
{ HOME: rootHome },
|
|
1366
|
+
)
|
|
1367
|
+
|
|
1368
|
+
const harmonyRoot = path.join(tmpRoot, 'demo_harmony')
|
|
1369
|
+
assert(
|
|
1370
|
+
!exists(path.join(harmonyRoot, 'lib/core/base/base_service.dart')),
|
|
1371
|
+
'base + no-network should not generate BaseService',
|
|
1372
|
+
)
|
|
1373
|
+
assert(
|
|
1374
|
+
exists(path.join(harmonyRoot, 'lib/helpers/README.platform-permissions.md')),
|
|
1375
|
+
'harmony helper permission README missing',
|
|
1376
|
+
)
|
|
1377
|
+
assert(
|
|
1378
|
+
exists(path.join(harmonyRoot, 'lib/helpers/webview/README.md')),
|
|
1379
|
+
'harmony webview helper README missing',
|
|
1380
|
+
)
|
|
1381
|
+
assert(
|
|
1382
|
+
fileIncludes(path.join(harmonyRoot, 'lib/helpers/webview/README.md'), 'Harmony'),
|
|
1383
|
+
'harmony helper README should mention Harmony',
|
|
1384
|
+
)
|
|
1385
|
+
const harmonyPubspec = path.join(harmonyRoot, 'pubspec.yaml')
|
|
1386
|
+
assert(
|
|
1387
|
+
fileIncludes(harmonyPubspec, 'webview_flutter:'),
|
|
1388
|
+
'harmony project should keep webview_flutter for common platforms',
|
|
1389
|
+
)
|
|
1390
|
+
assert(
|
|
1391
|
+
fileIncludes(harmonyPubspec, 'permission_handler:'),
|
|
1392
|
+
'harmony project should keep permission_handler for common platforms',
|
|
1393
|
+
)
|
|
1394
|
+
assert(
|
|
1395
|
+
fileIncludes(harmonyPubspec, 'image_picker:'),
|
|
1396
|
+
'harmony project should keep image_picker for common platforms',
|
|
1397
|
+
)
|
|
1398
|
+
assert(
|
|
1399
|
+
exists(path.join(harmonyRoot, '.flu-cli.json')),
|
|
1400
|
+
'built-in harmony lite project should also write .flu-cli.json',
|
|
1401
|
+
)
|
|
1402
|
+
assert(
|
|
1403
|
+
!exists(path.join(harmonyRoot, 'lib/services/home_feed_service.dart')),
|
|
1404
|
+
'no-network project should not generate network-backed home feed service',
|
|
1405
|
+
)
|
|
1406
|
+
assert(
|
|
1407
|
+
fileIncludes(
|
|
1408
|
+
path.join(harmonyRoot, 'ohos/entry/src/main/module.json5'),
|
|
1409
|
+
'ohos.permission.CAMERA',
|
|
1410
|
+
),
|
|
1411
|
+
'harmony helper should inject camera permission into module.json5',
|
|
1412
|
+
)
|
|
1413
|
+
assert(
|
|
1414
|
+
fileIncludes(
|
|
1415
|
+
path.join(harmonyRoot, 'ohos/entry/src/main/module.json5'),
|
|
1416
|
+
'ohos.permission.READ_IMAGEVIDEO',
|
|
1417
|
+
),
|
|
1418
|
+
'harmony helper should inject photo permission into module.json5',
|
|
1419
|
+
)
|
|
1420
|
+
assertProjectImportsResolvable(harmonyRoot)
|
|
1421
|
+
|
|
1422
|
+
runCli(
|
|
1423
|
+
[
|
|
1424
|
+
'new',
|
|
1425
|
+
'demo_permission_only',
|
|
1426
|
+
'--template',
|
|
1427
|
+
'lite',
|
|
1428
|
+
'--state',
|
|
1429
|
+
'provider',
|
|
1430
|
+
'--dir',
|
|
1431
|
+
tmpRoot,
|
|
1432
|
+
'--package',
|
|
1433
|
+
'com.example.demo_permission_only',
|
|
1434
|
+
'--author',
|
|
1435
|
+
'test',
|
|
1436
|
+
'--helpers',
|
|
1437
|
+
'permission',
|
|
1438
|
+
'--no-network',
|
|
1439
|
+
'--flutter-sdk',
|
|
1440
|
+
'custom',
|
|
1441
|
+
'--flutter-bin',
|
|
1442
|
+
fakeFlutterBin,
|
|
1443
|
+
],
|
|
1444
|
+
repoRoot,
|
|
1445
|
+
{ HOME: rootHome },
|
|
1446
|
+
)
|
|
1447
|
+
|
|
1448
|
+
const permissionOnlyRoot = path.join(tmpRoot, 'demo_permission_only')
|
|
1449
|
+
assert(
|
|
1450
|
+
fileIncludes(
|
|
1451
|
+
path.join(permissionOnlyRoot, 'android/app/src/main/AndroidManifest.xml'),
|
|
1452
|
+
'android.permission.CAMERA',
|
|
1453
|
+
),
|
|
1454
|
+
'permission helper should inject Android camera permission even without examples',
|
|
1455
|
+
)
|
|
1456
|
+
assert(
|
|
1457
|
+
fileIncludes(
|
|
1458
|
+
path.join(permissionOnlyRoot, 'ios/Runner/Info.plist'),
|
|
1459
|
+
'NSPhotoLibraryUsageDescription',
|
|
1460
|
+
),
|
|
1461
|
+
'permission helper should inject iOS photo description even without examples',
|
|
1462
|
+
)
|
|
1463
|
+
assert(
|
|
1464
|
+
fileIncludes(path.join(permissionOnlyRoot, 'ios/Podfile'), 'PERMISSION_CAMERA=1'),
|
|
1465
|
+
'permission helper should inject iOS camera Podfile macro even without examples',
|
|
1466
|
+
)
|
|
1467
|
+
assertHomeHasNoExampleModule(
|
|
1468
|
+
path.join(permissionOnlyRoot, 'lib/pages/home_page.dart'),
|
|
1469
|
+
'permission helper only',
|
|
1470
|
+
)
|
|
1471
|
+
|
|
1472
|
+
const structureCases = [
|
|
1473
|
+
{
|
|
1474
|
+
template: 'modular',
|
|
1475
|
+
projectName: 'demo_modular',
|
|
1476
|
+
args: ['--auth'],
|
|
1477
|
+
expectedFiles: [
|
|
1478
|
+
'lib/features/home/pages/home_page.dart',
|
|
1479
|
+
'lib/core/network/app_http.dart',
|
|
1480
|
+
'lib/core/auth/auth_service.dart',
|
|
1481
|
+
],
|
|
1482
|
+
expectedDescription: 'Flu CLI Modular 模板',
|
|
1483
|
+
},
|
|
1484
|
+
{
|
|
1485
|
+
template: 'clean',
|
|
1486
|
+
projectName: 'demo_clean',
|
|
1487
|
+
args: ['--no-network'],
|
|
1488
|
+
expectedFiles: ['lib/features/home/presentation/pages/home_page.dart'],
|
|
1489
|
+
expectedDescription: 'Flu CLI Clean 模板',
|
|
1490
|
+
},
|
|
1491
|
+
{
|
|
1492
|
+
template: 'clean',
|
|
1493
|
+
projectName: 'demo_clean_network',
|
|
1494
|
+
args: [],
|
|
1495
|
+
expectedFiles: [
|
|
1496
|
+
'lib/features/home/presentation/pages/home_page.dart',
|
|
1497
|
+
'lib/features/home/presentation/viewmodels/home_viewmodel.dart',
|
|
1498
|
+
'lib/features/home/presentation/pages/index.dart',
|
|
1499
|
+
],
|
|
1500
|
+
expectedDescription: 'Flu CLI Clean 模板',
|
|
1501
|
+
pubspecMustInclude: [
|
|
1502
|
+
'dio:',
|
|
1503
|
+
'connectivity_plus:',
|
|
1504
|
+
'json_annotation:',
|
|
1505
|
+
'build_runner:',
|
|
1506
|
+
'json_serializable:',
|
|
1507
|
+
],
|
|
1508
|
+
fluConfigMustInclude: ['"strategy": "json_serializable"'],
|
|
1509
|
+
extraCliArgs: ['--serialization', 'json_serializable'],
|
|
1510
|
+
},
|
|
1511
|
+
{
|
|
1512
|
+
template: 'modular',
|
|
1513
|
+
projectName: 'demo_modular_gallery',
|
|
1514
|
+
args: ['--examples', 'networkGallery'],
|
|
1515
|
+
expectedFiles: [
|
|
1516
|
+
'lib/features/home/pages/home_page.dart',
|
|
1517
|
+
'lib/features/user/pages/user_list_page.dart',
|
|
1518
|
+
'lib/core/network/app_http.dart',
|
|
1519
|
+
],
|
|
1520
|
+
expectedDescription: 'Flu CLI Modular 模板',
|
|
1521
|
+
},
|
|
1522
|
+
{
|
|
1523
|
+
template: 'clean',
|
|
1524
|
+
projectName: 'demo_clean_network_gallery',
|
|
1525
|
+
args: ['--examples', 'networkGallery'],
|
|
1526
|
+
expectedFiles: [
|
|
1527
|
+
'lib/features/home/presentation/pages/home_page.dart',
|
|
1528
|
+
'lib/features/home/presentation/viewmodels/home_viewmodel.dart',
|
|
1529
|
+
'lib/core/network/app_http.dart',
|
|
1530
|
+
],
|
|
1531
|
+
expectedDescription: 'Flu CLI Clean 模板',
|
|
1532
|
+
},
|
|
1533
|
+
]
|
|
1534
|
+
|
|
1535
|
+
for (const item of structureCases) {
|
|
1536
|
+
runCli(
|
|
1537
|
+
[
|
|
1538
|
+
'new',
|
|
1539
|
+
item.projectName,
|
|
1540
|
+
'--template',
|
|
1541
|
+
item.template,
|
|
1542
|
+
'--state',
|
|
1543
|
+
'provider',
|
|
1544
|
+
'--dir',
|
|
1545
|
+
tmpRoot,
|
|
1546
|
+
'--package',
|
|
1547
|
+
`com.example.${item.projectName}`,
|
|
1548
|
+
'--author',
|
|
1549
|
+
'test',
|
|
1550
|
+
'--flutter-sdk',
|
|
1551
|
+
'custom',
|
|
1552
|
+
'--flutter-bin',
|
|
1553
|
+
fakeFlutterBin,
|
|
1554
|
+
...(item.extraCliArgs || []),
|
|
1555
|
+
...item.args,
|
|
1556
|
+
],
|
|
1557
|
+
repoRoot,
|
|
1558
|
+
{ HOME: rootHome },
|
|
1559
|
+
)
|
|
1560
|
+
|
|
1561
|
+
const projectRoot = path.join(tmpRoot, item.projectName)
|
|
1562
|
+
for (const expectedFile of item.expectedFiles) {
|
|
1563
|
+
assert(
|
|
1564
|
+
exists(path.join(projectRoot, expectedFile)),
|
|
1565
|
+
`${item.template} project missing ${expectedFile}`,
|
|
1566
|
+
)
|
|
1567
|
+
}
|
|
1568
|
+
assert(
|
|
1569
|
+
fileIncludes(path.join(projectRoot, 'pubspec.yaml'), item.expectedDescription),
|
|
1570
|
+
`${item.template} pubspec should include template-specific description`,
|
|
1571
|
+
)
|
|
1572
|
+
for (const needle of item.pubspecMustInclude || []) {
|
|
1573
|
+
assert(
|
|
1574
|
+
fileIncludes(path.join(projectRoot, 'pubspec.yaml'), needle),
|
|
1575
|
+
`${item.template} pubspec should include ${needle}`,
|
|
1576
|
+
)
|
|
1577
|
+
}
|
|
1578
|
+
for (const needle of item.fluConfigMustInclude || []) {
|
|
1579
|
+
assert(
|
|
1580
|
+
fileIncludes(path.join(projectRoot, '.flu-cli.json'), needle),
|
|
1581
|
+
`${item.template} .flu-cli.json should include ${needle}`,
|
|
1582
|
+
)
|
|
1583
|
+
}
|
|
1584
|
+
const homePath = homePagePathForTemplate(projectRoot, item.template)
|
|
1585
|
+
assertHomeHasNoStarterNoise(homePath, item.projectName)
|
|
1586
|
+
if (item.args.includes('--examples')) {
|
|
1587
|
+
assertHomeHasExampleModule(homePath, item.projectName)
|
|
1588
|
+
} else {
|
|
1589
|
+
assertHomeHasNoExampleModule(homePath, item.projectName)
|
|
1590
|
+
}
|
|
1591
|
+
assertProjectImportsResolvable(projectRoot)
|
|
1592
|
+
|
|
1593
|
+
if (item.projectName === 'demo_modular') {
|
|
1594
|
+
assert(
|
|
1595
|
+
!exists(path.join(projectRoot, 'lib/features/user/services/home_feed_service.dart')),
|
|
1596
|
+
'modular network project without networkGallery should not generate home feed service',
|
|
1597
|
+
)
|
|
1598
|
+
assert(
|
|
1599
|
+
!exists(path.join(projectRoot, 'lib/features/user/viewmodels/user_list_viewmodel.dart')),
|
|
1600
|
+
'modular network project without networkGallery should not keep legacy user list viewmodel',
|
|
1601
|
+
)
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
if (item.projectName === 'demo_clean') {
|
|
1605
|
+
assert(
|
|
1606
|
+
!exists(
|
|
1607
|
+
path.join(projectRoot, 'lib/features/user/data/datasources/home_feed_service.dart'),
|
|
1608
|
+
),
|
|
1609
|
+
'clean no-network project should not generate home feed service',
|
|
1610
|
+
)
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
if (item.projectName === 'demo_clean_network') {
|
|
1614
|
+
assert(
|
|
1615
|
+
!exists(
|
|
1616
|
+
path.join(projectRoot, 'lib/features/user/data/datasources/home_feed_service.dart'),
|
|
1617
|
+
),
|
|
1618
|
+
'clean network project without networkGallery should not generate home feed service',
|
|
1619
|
+
)
|
|
1620
|
+
assert(
|
|
1621
|
+
!exists(
|
|
1622
|
+
path.join(
|
|
1623
|
+
projectRoot,
|
|
1624
|
+
'lib/features/user/presentation/viewmodels/user_list_viewmodel.dart',
|
|
1625
|
+
),
|
|
1626
|
+
),
|
|
1627
|
+
'clean network project without networkGallery should not keep legacy user list viewmodel',
|
|
1628
|
+
)
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
if (item.projectName === 'demo_modular_gallery') {
|
|
1632
|
+
assert(
|
|
1633
|
+
exists(path.join(projectRoot, 'lib/features/user/services/home_feed_service.dart')),
|
|
1634
|
+
'modular networkGallery should generate home feed service',
|
|
1635
|
+
)
|
|
1636
|
+
assert(
|
|
1637
|
+
fileIncludes(
|
|
1638
|
+
path.join(projectRoot, 'lib/features/user/viewmodels/user_list_viewmodel.dart'),
|
|
1639
|
+
'HomeFeedService',
|
|
1640
|
+
),
|
|
1641
|
+
'modular networkGallery list sample should use HomeFeedService',
|
|
1642
|
+
)
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
if (item.projectName === 'demo_clean_network_gallery') {
|
|
1646
|
+
assert(
|
|
1647
|
+
exists(path.join(projectRoot, 'lib/features/user/data/datasources/home_feed_service.dart')),
|
|
1648
|
+
'clean networkGallery should generate home feed service',
|
|
1649
|
+
)
|
|
1650
|
+
assert(
|
|
1651
|
+
fileIncludes(
|
|
1652
|
+
path.join(
|
|
1653
|
+
projectRoot,
|
|
1654
|
+
'lib/features/user/presentation/viewmodels/user_list_viewmodel.dart',
|
|
1655
|
+
),
|
|
1656
|
+
'HomeFeedService',
|
|
1657
|
+
),
|
|
1658
|
+
'clean networkGallery list sample should use HomeFeedService',
|
|
1659
|
+
)
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
if (item.template === 'modular') {
|
|
1663
|
+
assertAddPageWorks(
|
|
1664
|
+
projectRoot,
|
|
1665
|
+
'smoke_accept_page',
|
|
1666
|
+
'lib/features/smoke_accept_page/pages/smoke_accept_page_page.dart',
|
|
1667
|
+
'lib/features/smoke_accept_page/viewmodels/smoke_accept_page_viewmodel.dart',
|
|
1668
|
+
['--feature', 'smoke_accept_page'],
|
|
1669
|
+
)
|
|
1670
|
+
}
|
|
1671
|
+
if (item.template === 'clean' && item.projectName === 'demo_clean') {
|
|
1672
|
+
assertAddPageWorks(
|
|
1673
|
+
projectRoot,
|
|
1674
|
+
'smoke_accept_page',
|
|
1675
|
+
'lib/features/smoke_accept_page/presentation/pages/smoke_accept_page_page.dart',
|
|
1676
|
+
'lib/features/smoke_accept_page/presentation/viewmodels/smoke_accept_page_viewmodel.dart',
|
|
1677
|
+
['--feature', 'smoke_accept_page'],
|
|
1678
|
+
)
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
runCli(
|
|
1683
|
+
[
|
|
1684
|
+
'new',
|
|
1685
|
+
'demo_native',
|
|
1686
|
+
'--template',
|
|
1687
|
+
'native',
|
|
1688
|
+
'--dir',
|
|
1689
|
+
tmpRoot,
|
|
1690
|
+
'--package',
|
|
1691
|
+
'com.example.demo_native',
|
|
1692
|
+
'--author',
|
|
1693
|
+
'test',
|
|
1694
|
+
'--flutter-sdk',
|
|
1695
|
+
'custom',
|
|
1696
|
+
'--flutter-bin',
|
|
1697
|
+
fakeFlutterBin,
|
|
1698
|
+
],
|
|
1699
|
+
repoRoot,
|
|
1700
|
+
{ HOME: rootHome },
|
|
1701
|
+
)
|
|
1702
|
+
|
|
1703
|
+
const nativeRoot = path.join(tmpRoot, 'demo_native')
|
|
1704
|
+
assert(
|
|
1705
|
+
exists(path.join(nativeRoot, 'lib', 'main.dart')),
|
|
1706
|
+
'native project should include lib/main.dart',
|
|
1707
|
+
)
|
|
1708
|
+
assert(exists(path.join(nativeRoot, 'android')), 'native project should include android platform')
|
|
1709
|
+
assert(exists(path.join(nativeRoot, 'ios')), 'native project should include ios platform')
|
|
1710
|
+
assert(
|
|
1711
|
+
!exists(path.join(nativeRoot, 'lib', 'core')),
|
|
1712
|
+
'native project should not inject Flu core files',
|
|
1713
|
+
)
|
|
1714
|
+
assert(
|
|
1715
|
+
!exists(path.join(nativeRoot, '.flu-cli.json')),
|
|
1716
|
+
'native project should not write .flu-cli.json by default',
|
|
1717
|
+
)
|
|
1718
|
+
|
|
1719
|
+
runCli(
|
|
1720
|
+
[
|
|
1721
|
+
'new',
|
|
1722
|
+
'demo_native_webview',
|
|
1723
|
+
'--template',
|
|
1724
|
+
'native',
|
|
1725
|
+
'--dir',
|
|
1726
|
+
tmpRoot,
|
|
1727
|
+
'--package',
|
|
1728
|
+
'com.example.demo_native_webview',
|
|
1729
|
+
'--author',
|
|
1730
|
+
'test',
|
|
1731
|
+
'--helpers',
|
|
1732
|
+
'webview',
|
|
1733
|
+
'--no-network',
|
|
1734
|
+
'--flutter-sdk',
|
|
1735
|
+
'custom',
|
|
1736
|
+
'--flutter-bin',
|
|
1737
|
+
fakeFlutterBin,
|
|
1738
|
+
],
|
|
1739
|
+
repoRoot,
|
|
1740
|
+
{ HOME: rootHome },
|
|
1741
|
+
)
|
|
1742
|
+
|
|
1743
|
+
const nativeWebviewRoot = path.join(tmpRoot, 'demo_native_webview')
|
|
1744
|
+
assert(
|
|
1745
|
+
exists(path.join(nativeWebviewRoot, 'lib', 'helpers', 'webview')),
|
|
1746
|
+
'native + webview helper should inject lib/helpers/webview via postSkeletonEnrich',
|
|
1747
|
+
)
|
|
1748
|
+
assert(
|
|
1749
|
+
!exists(path.join(nativeWebviewRoot, 'lib', 'core')),
|
|
1750
|
+
'native + helper should still not inject Flu core',
|
|
1751
|
+
)
|
|
1752
|
+
assert(
|
|
1753
|
+
fileIncludes(path.join(nativeWebviewRoot, 'pubspec.yaml'), 'webview_flutter:'),
|
|
1754
|
+
'native + webview helper should add webview_flutter to pubspec.yaml',
|
|
1755
|
+
)
|
|
1756
|
+
assert(
|
|
1757
|
+
fileIncludes(path.join(nativeWebviewRoot, 'test', 'widget_test.dart'), '/main.dart'),
|
|
1758
|
+
'native + webview widget_test should import main.dart',
|
|
1759
|
+
)
|
|
1760
|
+
|
|
1761
|
+
runCli(
|
|
1762
|
+
[
|
|
1763
|
+
'new',
|
|
1764
|
+
'demo_native_network',
|
|
1765
|
+
'--template',
|
|
1766
|
+
'native',
|
|
1767
|
+
'--dir',
|
|
1768
|
+
tmpRoot,
|
|
1769
|
+
'--package',
|
|
1770
|
+
'com.example.demo_native_network',
|
|
1771
|
+
'--author',
|
|
1772
|
+
'test',
|
|
1773
|
+
'--network',
|
|
1774
|
+
'--flutter-sdk',
|
|
1775
|
+
'custom',
|
|
1776
|
+
'--flutter-bin',
|
|
1777
|
+
fakeFlutterBin,
|
|
1778
|
+
],
|
|
1779
|
+
repoRoot,
|
|
1780
|
+
{ HOME: rootHome },
|
|
1781
|
+
)
|
|
1782
|
+
|
|
1783
|
+
const nativeNetworkRoot = path.join(tmpRoot, 'demo_native_network')
|
|
1784
|
+
assert(
|
|
1785
|
+
exists(path.join(nativeNetworkRoot, 'lib', 'core', 'network', 'app_http.dart')),
|
|
1786
|
+
'native + network should inject lib/core/network via postSkeletonEnrich',
|
|
1787
|
+
)
|
|
1788
|
+
assert(
|
|
1789
|
+
fileIncludes(path.join(nativeNetworkRoot, 'pubspec.yaml'), 'dio:'),
|
|
1790
|
+
'native + network should add dio to pubspec.yaml',
|
|
1791
|
+
)
|
|
1792
|
+
assert(
|
|
1793
|
+
fileIncludes(
|
|
1794
|
+
path.join(nativeNetworkRoot, 'lib', 'core', 'router', 'app_routes.dart'),
|
|
1795
|
+
'routes =>',
|
|
1796
|
+
),
|
|
1797
|
+
'native network project should patch app_routes placeholder',
|
|
1798
|
+
)
|
|
1799
|
+
assert(
|
|
1800
|
+
fileIncludes(path.join(nativeNetworkRoot, 'test', 'widget_test.dart'), '/main.dart'),
|
|
1801
|
+
'native project widget_test should import main.dart instead of app.dart',
|
|
1802
|
+
)
|
|
1803
|
+
assert(
|
|
1804
|
+
!fileIncludes(path.join(nativeNetworkRoot, 'test', 'widget_test.dart'), '/app.dart'),
|
|
1805
|
+
'native project widget_test should not import missing app.dart',
|
|
1806
|
+
)
|
|
1807
|
+
|
|
1808
|
+
const blueprintRoot = path.join(tmpRoot, 'demo_modular')
|
|
1809
|
+
fs.writeFileSync(
|
|
1810
|
+
path.join(blueprintRoot, 'flu-blueprint.yaml'),
|
|
1811
|
+
[
|
|
1812
|
+
"version: '1'",
|
|
1813
|
+
'features:',
|
|
1814
|
+
' - name: order',
|
|
1815
|
+
' pages:',
|
|
1816
|
+
' - name: order_list',
|
|
1817
|
+
' type: list-page',
|
|
1818
|
+
' model: order',
|
|
1819
|
+
' services:',
|
|
1820
|
+
' - order',
|
|
1821
|
+
' models:',
|
|
1822
|
+
' - order',
|
|
1823
|
+
'',
|
|
1824
|
+
].join('\n'),
|
|
1825
|
+
)
|
|
1826
|
+
const blueprintResult = runCliJson(['blueprint', 'gen', '--dir', blueprintRoot], repoRoot, {
|
|
1827
|
+
HOME: rootHome,
|
|
1828
|
+
})
|
|
1829
|
+
assert(
|
|
1830
|
+
blueprintResult.data.generated.includes('ListPage:order_list'),
|
|
1831
|
+
'blueprint should generate list page',
|
|
1832
|
+
)
|
|
1833
|
+
assert(
|
|
1834
|
+
blueprintResult.data.generated.includes('Service:order'),
|
|
1835
|
+
'blueprint should generate service',
|
|
1836
|
+
)
|
|
1837
|
+
assert(blueprintResult.data.generated.includes('Model:order'), 'blueprint should generate model')
|
|
1838
|
+
const blueprintPage = fs.readFileSync(
|
|
1839
|
+
path.join(blueprintRoot, 'lib/features/order/pages/order_list_page.dart'),
|
|
1840
|
+
'utf8',
|
|
1841
|
+
)
|
|
1842
|
+
assert(
|
|
1843
|
+
!blueprintPage.includes('item.name'),
|
|
1844
|
+
'blueprint list page should not assume generated models contain a name field',
|
|
1845
|
+
)
|
|
1846
|
+
assert(
|
|
1847
|
+
!blueprintPage.includes('package:provider/provider.dart'),
|
|
1848
|
+
'blueprint generated page should not import unused provider package',
|
|
1849
|
+
)
|
|
1850
|
+
assertProjectImportsResolvable(blueprintRoot)
|
|
1851
|
+
|
|
1852
|
+
const customTemplateRoot = path.join(tmpRoot, 'custom_template')
|
|
1853
|
+
fs.mkdirSync(path.join(customTemplateRoot, 'lib'), { recursive: true })
|
|
1854
|
+
fs.writeFileSync(
|
|
1855
|
+
path.join(customTemplateRoot, 'pubspec.yaml'),
|
|
1856
|
+
[
|
|
1857
|
+
'name: custom_template',
|
|
1858
|
+
'description: local custom template',
|
|
1859
|
+
'publish_to: none',
|
|
1860
|
+
'environment:',
|
|
1861
|
+
' sdk: "^3.0.0"',
|
|
1862
|
+
'dependencies:',
|
|
1863
|
+
' flutter:',
|
|
1864
|
+
' sdk: flutter',
|
|
1865
|
+
'dev_dependencies:',
|
|
1866
|
+
' flutter_test:',
|
|
1867
|
+
' sdk: flutter',
|
|
1868
|
+
'flutter:',
|
|
1869
|
+
' uses-material-design: true',
|
|
1870
|
+
'',
|
|
1871
|
+
].join('\n'),
|
|
1872
|
+
)
|
|
1873
|
+
fs.writeFileSync(path.join(customTemplateRoot, 'lib', 'main.dart'), 'void main() {}\n')
|
|
1874
|
+
|
|
1875
|
+
const configDir = path.join(rootHome, '.flu-cli')
|
|
1876
|
+
const configPath = path.join(configDir, 'config.json')
|
|
1877
|
+
fs.mkdirSync(configDir, { recursive: true })
|
|
1878
|
+
fs.writeFileSync(
|
|
1879
|
+
configPath,
|
|
1880
|
+
JSON.stringify(
|
|
1881
|
+
{
|
|
1882
|
+
templates: [
|
|
1883
|
+
{
|
|
1884
|
+
id: 'local-smoke-template',
|
|
1885
|
+
name: 'Local Smoke Template',
|
|
1886
|
+
type: 'local',
|
|
1887
|
+
path: customTemplateRoot,
|
|
1888
|
+
},
|
|
1889
|
+
],
|
|
1890
|
+
},
|
|
1891
|
+
null,
|
|
1892
|
+
2,
|
|
1893
|
+
),
|
|
1894
|
+
)
|
|
1895
|
+
|
|
1896
|
+
runCli(
|
|
1897
|
+
[
|
|
1898
|
+
'new',
|
|
1899
|
+
'demo_custom_local',
|
|
1900
|
+
'--template',
|
|
1901
|
+
'local-smoke-template',
|
|
1902
|
+
'--dir',
|
|
1903
|
+
tmpRoot,
|
|
1904
|
+
'--package',
|
|
1905
|
+
'com.example.demo_custom_local',
|
|
1906
|
+
'--author',
|
|
1907
|
+
'test',
|
|
1908
|
+
'--flutter-sdk',
|
|
1909
|
+
'custom',
|
|
1910
|
+
'--flutter-bin',
|
|
1911
|
+
fakeFlutterBin,
|
|
1912
|
+
],
|
|
1913
|
+
repoRoot,
|
|
1914
|
+
{ HOME: rootHome },
|
|
1915
|
+
)
|
|
1916
|
+
|
|
1917
|
+
const customRoot = path.join(tmpRoot, 'demo_custom_local')
|
|
1918
|
+
assert(
|
|
1919
|
+
exists(path.join(customRoot, 'pubspec.yaml')),
|
|
1920
|
+
'custom local template project should keep pubspec.yaml',
|
|
1921
|
+
)
|
|
1922
|
+
assert(
|
|
1923
|
+
!exists(path.join(customRoot, '.flu-cli.json')),
|
|
1924
|
+
'custom local template should not auto-write .flu-cli.json',
|
|
1925
|
+
)
|
|
1926
|
+
assert(
|
|
1927
|
+
exists(path.join(customRoot, 'lib', 'core')),
|
|
1928
|
+
'custom local template should still receive default Flu core when not marked native',
|
|
1929
|
+
)
|
|
1930
|
+
|
|
1931
|
+
console.log('cli vNext generate smoke: ok')
|
|
1932
|
+
} finally {
|
|
1933
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true })
|
|
1934
|
+
}
|