1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:args/args.dart';
6import 'package:meta/meta.dart';
7import 'package:package_config/package_config.dart';
8
9import '../base/common.dart';
10import '../base/deferred_component.dart';
11import '../base/file_system.dart';
12import '../base/logger.dart';
13import '../base/os.dart';
14import '../base/platform.dart';
15import '../base/process.dart';
16import '../build_info.dart';
17import '../bundle.dart' as bundle;
18import '../cache.dart';
19import '../convert.dart';
20import '../dart/pub.dart';
21import '../device.dart';
22import '../flutter_manifest.dart';
23import '../linux/build_linux.dart';
24import '../macos/build_macos.dart';
25import '../project.dart';
26import '../runner/flutter_command.dart';
27import '../widget_preview/preview_code_generator.dart';
28import '../widget_preview/preview_detector.dart';
29import '../widget_preview/preview_manifest.dart';
30import '../windows/build_windows.dart';
31import 'create_base.dart';
32import 'daemon.dart';
33
34class WidgetPreviewCommand extends FlutterCommand {
35 WidgetPreviewCommand({
36 required bool verboseHelp,
37 required Logger logger,
38 required FileSystem fs,
39 required FlutterProjectFactory projectFactory,
40 required Cache cache,
41 required Platform platform,
42 required ShutdownHooks shutdownHooks,
43 required OperatingSystemUtils os,
44 }) {
45 addSubcommand(
46 WidgetPreviewStartCommand(
47 verboseHelp: verboseHelp,
48 logger: logger,
49 fs: fs,
50 projectFactory: projectFactory,
51 cache: cache,
52 platform: platform,
53 shutdownHooks: shutdownHooks,
54 os: os,
55 ),
56 );
57 addSubcommand(
58 WidgetPreviewCleanCommand(logger: logger, fs: fs, projectFactory: projectFactory),
59 );
60 }
61
62 @override
63 String get description => 'Manage the widget preview environment.';
64
65 @override
66 String get name => 'widget-preview';
67
68 @override
69 String get category => FlutterCommandCategory.tools;
70
71 // TODO(bkonyi): show when --verbose is not provided when this feature is
72 // ready to ship.
73 @override
74 bool get hidden => true;
75
76 @override
77 Future<FlutterCommandResult> runCommand() async => FlutterCommandResult.fail();
78}
79
80abstract base class WidgetPreviewSubCommandBase extends FlutterCommand {
81 FileSystem get fs;
82 Logger get logger;
83 FlutterProjectFactory get projectFactory;
84
85 FlutterProject getRootProject() {
86 final ArgResults results = argResults!;
87 final Directory projectDir;
88 if (results.rest case <String>[final String directory]) {
89 projectDir = fs.directory(directory);
90 if (!projectDir.existsSync()) {
91 throwToolExit('Could not find ${projectDir.path}.');
92 }
93 } else if (results.rest.length > 1) {
94 throwToolExit('Only one directory should be provided.');
95 } else {
96 projectDir = fs.currentDirectory;
97 }
98 return validateFlutterProjectForPreview(projectDir);
99 }
100
101 FlutterProject validateFlutterProjectForPreview(Directory directory) {
102 logger.printTrace('Verifying that ${directory.path} is a Flutter project.');
103 final FlutterProject flutterProject = projectFactory.fromDirectory(directory);
104 if (!flutterProject.dartTool.existsSync()) {
105 throwToolExit('${flutterProject.directory.path} is not a valid Flutter project.');
106 }
107 return flutterProject;
108 }
109}
110
111final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with CreateBase {
112 WidgetPreviewStartCommand({
113 this.verboseHelp = false,
114 required this.logger,
115 required this.fs,
116 required this.projectFactory,
117 required this.cache,
118 required this.platform,
119 required this.shutdownHooks,
120 required this.os,
121 }) {
122 addPubOptions();
123 argParser
124 ..addFlag(
125 kLaunchPreviewer,
126 defaultsTo: true,
127 help: 'Launches the widget preview environment.',
128 // Should only be used for testing.
129 hide: !verboseHelp,
130 )
131 ..addFlag(
132 kUseFlutterDesktop,
133 help: '(deprecated) Launches the widget preview environment using Flutter Desktop.',
134 hide: !verboseHelp,
135 )
136 ..addFlag(
137 kHeadlessWeb,
138 help: 'Launches Chrome in headless mode for testing.',
139 hide: !verboseHelp,
140 )
141 ..addOption(
142 kWidgetPreviewScaffoldOutputDir,
143 help:
144 'Generated the widget preview environment scaffolding at a given location '
145 'for testing purposes.',
146 );
147 }
148
149 static const String kWidgetPreviewScaffoldName = 'widget_preview_scaffold';
150 static const String kLaunchPreviewer = 'launch-previewer';
151 static const String kUseFlutterDesktop = 'desktop';
152 static const String kHeadlessWeb = 'headless-web';
153 static const String kWidgetPreviewScaffoldOutputDir = 'scaffold-output-dir';
154
155 @override
156 Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{
157 // Ensure the Flutter Web SDK is installed.
158 DevelopmentArtifact.web,
159 };
160
161 @override
162 String get description => 'Starts the widget preview environment.';
163
164 @override
165 String get name => 'start';
166
167 final bool verboseHelp;
168
169 bool get isWeb => !boolArg(kUseFlutterDesktop);
170
171 @override
172 final FileSystem fs;
173
174 @override
175 final Logger logger;
176
177 @override
178 final FlutterProjectFactory projectFactory;
179
180 final Cache cache;
181
182 final Platform platform;
183
184 final ShutdownHooks shutdownHooks;
185
186 final OperatingSystemUtils os;
187
188 late final FlutterProject rootProject = getRootProject();
189
190 late final PreviewDetector _previewDetector = PreviewDetector(
191 projectRoot: rootProject.directory,
192 logger: logger,
193 fs: fs,
194 onChangeDetected: onChangeDetected,
195 onPubspecChangeDetected: onPubspecChangeDetected,
196 );
197
198 late final PreviewCodeGenerator _previewCodeGenerator;
199 late final PreviewManifest _previewManifest = PreviewManifest(
200 logger: logger,
201 rootProject: rootProject,
202 fs: fs,
203 cache: cache,
204 );
205
206 /// The currently running instance of the widget preview scaffold.
207 AppInstance? _widgetPreviewApp;
208
209 @override
210 Future<FlutterCommandResult> runCommand() async {
211 final String? customPreviewScaffoldOutput = stringArg(kWidgetPreviewScaffoldOutputDir);
212 final Directory widgetPreviewScaffold =
213 customPreviewScaffoldOutput != null
214 ? fs.directory(customPreviewScaffoldOutput)
215 : rootProject.widgetPreviewScaffold;
216
217 // Check to see if a preview scaffold has already been generated. If not,
218 // generate one.
219 final bool generateScaffoldProject =
220 customPreviewScaffoldOutput != null || _previewManifest.shouldGenerateProject();
221 // TODO(bkonyi): can this be moved?
222 widgetPreviewScaffold.createSync();
223
224 if (generateScaffoldProject) {
225 // WARNING: this log message is used by test/integration.shard/widget_preview_test.dart
226 logger.printStatus(
227 'Creating widget preview scaffolding at: ${widgetPreviewScaffold.absolute.path}',
228 );
229 await generateApp(
230 <String>['app', kWidgetPreviewScaffoldName],
231 widgetPreviewScaffold,
232 createTemplateContext(
233 organization: 'flutter',
234 projectName: kWidgetPreviewScaffoldName,
235 titleCaseProjectName: 'Widget Preview Scaffold',
236 flutterRoot: Cache.flutterRoot!,
237 dartSdkVersionBounds: '^${cache.dartSdkBuild}',
238 linux: platform.isLinux && !isWeb,
239 macos: platform.isMacOS && !isWeb,
240 windows: platform.isWindows && !isWeb,
241 web: isWeb,
242 ),
243 overwrite: true,
244 generateMetadata: false,
245 );
246 if (customPreviewScaffoldOutput != null) {
247 return FlutterCommandResult.success();
248 }
249 _previewManifest.generate();
250
251 // WARNING: this access of widgetPreviewScaffoldProject needs to happen
252 // after we generate the scaffold project as invoking the getter triggers
253 // lazy initialization of the preview scaffold's FlutterManifest before
254 // the scaffold project's pubspec has been generated.
255 // TODO(bkonyi): add logic to rebuild after SDK updates
256 await initialBuild(widgetPreviewScaffoldProject: rootProject.widgetPreviewScaffoldProject);
257 }
258
259 _previewCodeGenerator = PreviewCodeGenerator(
260 widgetPreviewScaffoldProject: rootProject.widgetPreviewScaffoldProject,
261 fs: fs,
262 );
263
264 if (generateScaffoldProject || _previewManifest.shouldRegeneratePubspec()) {
265 if (!generateScaffoldProject) {
266 logger.printStatus(
267 'Detected changes in pubspec.yaml. Regenerating pubspec.yaml for the '
268 'widget preview scaffold.',
269 );
270 }
271 // TODO(matanlurey): Remove this comment once flutter_gen is removed.
272 //
273 // Tracking removal: https://github.com/flutter/flutter/issues/102983.
274 //
275 // Populate the pubspec after the initial build to avoid blowing away the package_config.json
276 // which may have manual changes for flutter_gen support.
277 await _populatePreviewPubspec(rootProject: rootProject);
278 }
279
280 final PreviewMapping initialPreviews = await _previewDetector.initialize();
281 _previewCodeGenerator.populatePreviewsInGeneratedPreviewScaffold(initialPreviews);
282
283 if (boolArg(kLaunchPreviewer)) {
284 shutdownHooks.addShutdownHook(() async {
285 await _widgetPreviewApp?.stop();
286 });
287 _widgetPreviewApp = await runPreviewEnvironment(
288 widgetPreviewScaffoldProject: rootProject.widgetPreviewScaffoldProject,
289 );
290 final int result = await _widgetPreviewApp!.runner.waitForAppToFinish();
291 if (result != 0) {
292 throwToolExit('Failed to launch the widget previewer.', exitCode: result);
293 }
294 }
295
296 await _previewDetector.dispose();
297 return FlutterCommandResult.success();
298 }
299
300 void onChangeDetected(PreviewMapping previews) {
301 _previewCodeGenerator.populatePreviewsInGeneratedPreviewScaffold(previews);
302 logger.printStatus('Triggering reload based on change to preview set: $previews');
303 _widgetPreviewApp?.restart();
304 }
305
306 void onPubspecChangeDetected() {
307 // TODO(bkonyi): trigger hot reload or restart?
308 logger.printStatus('Changes to pubspec.yaml detected.');
309 _populatePreviewPubspec(rootProject: rootProject);
310 }
311
312 /// Builds the application binary for the widget preview scaffold the first
313 /// time the widget preview command is run.
314 ///
315 /// The resulting binary is used to speed up subsequent widget previewer launches
316 /// by acting as a basic scaffold to load previews into using hot reload / restart.
317 Future<void> initialBuild({required FlutterProject widgetPreviewScaffoldProject}) async {
318 // TODO(bkonyi): handle error case where desktop device isn't enabled.
319 await widgetPreviewScaffoldProject.ensureReadyForPlatformSpecificTooling(
320 releaseMode: false,
321 linuxPlatform: platform.isLinux && !isWeb,
322 macOSPlatform: platform.isMacOS && !isWeb,
323 windowsPlatform: platform.isWindows && !isWeb,
324 webPlatform: isWeb,
325 );
326
327 // Generate initial package_config.json, otherwise the build will fail.
328 await pub.get(
329 context: PubContext.create,
330 project: widgetPreviewScaffoldProject,
331 offline: offline,
332 outputMode: PubOutputMode.summaryOnly,
333 );
334
335 if (isWeb) {
336 return;
337 }
338
339 // WARNING: this log message is used by test/integration.shard/widget_preview_test.dart
340 logger.printStatus('Performing initial build of the Widget Preview Scaffold...');
341
342 final BuildInfo buildInfo = BuildInfo(
343 BuildMode.debug,
344 null,
345 treeShakeIcons: false,
346 packageConfigPath: widgetPreviewScaffoldProject.packageConfig.path,
347 );
348
349 if (platform.isMacOS) {
350 await buildMacOS(
351 flutterProject: widgetPreviewScaffoldProject,
352 buildInfo: buildInfo,
353 verboseLogging: false,
354 );
355 } else if (platform.isLinux) {
356 await buildLinux(
357 widgetPreviewScaffoldProject.linux,
358 buildInfo,
359 targetPlatform:
360 os.hostPlatform == HostPlatform.linux_x64
361 ? TargetPlatform.linux_x64
362 : TargetPlatform.linux_arm64,
363 logger: logger,
364 );
365 } else if (platform.isWindows) {
366 await buildWindows(
367 widgetPreviewScaffoldProject.windows,
368 buildInfo,
369 os.hostPlatform == HostPlatform.windows_x64
370 ? TargetPlatform.windows_x64
371 : TargetPlatform.windows_arm64,
372 );
373 } else {
374 throw UnimplementedError();
375 }
376 // WARNING: this log message is used by test/integration.shard/widget_preview_test.dart
377 logger.printStatus('Widget Preview Scaffold initial build complete.');
378 }
379
380 /// Returns the path to a prebuilt widget_preview_scaffold application binary.
381 String prebuiltApplicationBinaryPath({required FlutterProject widgetPreviewScaffoldProject}) {
382 assert(platform.isLinux || platform.isMacOS || platform.isWindows);
383 String path;
384 if (platform.isMacOS) {
385 path = fs.path.join(
386 getMacOSBuildDirectory(),
387 'Build/Products/Debug/widget_preview_scaffold.app',
388 );
389 } else if (platform.isLinux) {
390 path = fs.path.join(
391 getLinuxBuildDirectory(
392 os.hostPlatform == HostPlatform.linux_x64
393 ? TargetPlatform.linux_x64
394 : TargetPlatform.linux_arm64,
395 ),
396 'debug/bundle/widget_preview_scaffold',
397 );
398 } else if (platform.isWindows) {
399 path = fs.path.join(
400 getWindowsBuildDirectory(
401 os.hostPlatform == HostPlatform.windows_x64
402 ? TargetPlatform.windows_x64
403 : TargetPlatform.windows_arm64,
404 ),
405 'runner/Debug/widget_preview_scaffold.exe',
406 );
407 } else {
408 throw StateError('Unknown OS');
409 }
410 path = fs.path.join(widgetPreviewScaffoldProject.directory.path, path);
411 if (fs.typeSync(path) == FileSystemEntityType.notFound) {
412 logger.printStatus(fs.currentDirectory.toString());
413 throw StateError('Could not find prebuilt application binary at $path.');
414 }
415 return path;
416 }
417
418 Future<AppInstance> runPreviewEnvironment({
419 required FlutterProject widgetPreviewScaffoldProject,
420 }) async {
421 final AppInstance app;
422 try {
423 // Since the only target supported by the widget preview scaffold is the host's desktop
424 // device, only a single desktop device should be returned.
425 final List<Device> devices = await deviceManager!.getDevices(
426 filter: DeviceDiscoveryFilter(
427 supportFilter: DeviceDiscoverySupportFilter.excludeDevicesUnsupportedByFlutterOrProject(
428 flutterProject: widgetPreviewScaffoldProject,
429 ),
430 deviceConnectionInterface: DeviceConnectionInterface.attached,
431 ),
432 );
433 assert(devices.length == 1);
434 final Device device = devices.first;
435
436 // We launch from a prebuilt widget preview scaffold instance to reduce launch times after
437 // the first run.
438 File? prebuiltApplicationBinary;
439 if (!isWeb) {
440 prebuiltApplicationBinary = fs.file(
441 prebuiltApplicationBinaryPath(widgetPreviewScaffoldProject: widgetPreviewScaffoldProject),
442 );
443 }
444 const String? kEmptyRoute = null;
445 const bool kEnableHotReload = true;
446
447 // WARNING: this log message is used by test/integration.shard/widget_preview_test.dart
448 logger.printStatus('Launching the Widget Preview Scaffold...');
449
450 app = await Daemon.createMachineDaemon().appDomain.startApp(
451 device,
452 widgetPreviewScaffoldProject.directory.path,
453 bundle.defaultMainPath,
454 kEmptyRoute, // route
455 DebuggingOptions.enabled(
456 BuildInfo(
457 BuildMode.debug,
458 null,
459 treeShakeIcons: false,
460 extraFrontEndOptions:
461 isWeb ? <String>['--dartdevc-canary', '--dartdevc-module-format=ddc'] : null,
462 packageConfigPath: widgetPreviewScaffoldProject.packageConfig.path,
463 packageConfig: PackageConfig.parseBytes(
464 widgetPreviewScaffoldProject.packageConfig.readAsBytesSync(),
465 widgetPreviewScaffoldProject.packageConfig.uri,
466 ),
467 ),
468 webEnableExposeUrl: false,
469 webRunHeadless: boolArg(kHeadlessWeb),
470 ),
471 kEnableHotReload, // hot mode
472 applicationBinary: prebuiltApplicationBinary,
473 trackWidgetCreation: true,
474 projectRootPath: widgetPreviewScaffoldProject.directory.path,
475 );
476 } on Exception catch (error) {
477 throwToolExit(error.toString());
478 }
479
480 if (!isWeb) {
481 // Immediately perform a hot restart to ensure new previews are loaded into the prebuilt
482 // application.
483 // WARNING: this log message is used by test/integration.shard/widget_preview_test.dart
484 logger.printStatus('Loading previews into the Widget Preview Scaffold...');
485 await app.restart(fullRestart: true);
486 }
487 // WARNING: this log message is used by test/integration.shard/widget_preview_test.dart
488 logger.printStatus('Done loading previews.');
489 return app;
490 }
491
492 @visibleForTesting
493 static const Map<String, String> flutterGenPackageConfigEntry = <String, String>{
494 'name': 'flutter_gen',
495 'rootUri': '../../flutter_gen',
496 'languageVersion': '2.12',
497 };
498
499 /// Maps asset URIs to relative paths for the widget preview project to
500 /// include.
501 @visibleForTesting
502 static Uri transformAssetUri(Uri uri) {
503 // Assets provided by packages always start with 'packages' and do not
504 // require their URIs to be updated.
505 if (uri.path.startsWith('packages')) {
506 return uri;
507 }
508 // Otherwise, the asset is contained within the root project and needs
509 // to be referenced from the widget preview scaffold project's pubspec.
510 return Uri(path: '../../${uri.path}');
511 }
512
513 @visibleForTesting
514 static AssetsEntry transformAssetsEntry(AssetsEntry asset) {
515 return AssetsEntry(
516 uri: transformAssetUri(asset.uri),
517 flavors: asset.flavors,
518 transformers: asset.transformers,
519 );
520 }
521
522 @visibleForTesting
523 static FontAsset transformFontAsset(FontAsset asset) {
524 return FontAsset(transformAssetUri(asset.assetUri), weight: asset.weight, style: asset.style);
525 }
526
527 @visibleForTesting
528 static DeferredComponent transformDeferredComponent(DeferredComponent component) {
529 return DeferredComponent(
530 name: component.name,
531 // TODO(bkonyi): verify these library paths are always package: paths from the parent project.
532 libraries: component.libraries,
533 assets: component.assets.map(transformAssetsEntry).toList(),
534 );
535 }
536
537 @visibleForTesting
538 FlutterManifest buildPubspec({
539 required FlutterManifest rootManifest,
540 required FlutterManifest widgetPreviewManifest,
541 }) {
542 final List<AssetsEntry> assets = rootManifest.assets.map(transformAssetsEntry).toList();
543
544 final List<Font> fonts = <Font>[
545 ...widgetPreviewManifest.fonts,
546 ...rootManifest.fonts.map((Font font) {
547 return Font(font.familyName, font.fontAssets.map(transformFontAsset).toList());
548 }),
549 ];
550
551 final List<Uri> shaders = rootManifest.shaders.map(transformAssetUri).toList();
552
553 final List<DeferredComponent>? deferredComponents =
554 rootManifest.deferredComponents?.map(transformDeferredComponent).toList();
555
556 return widgetPreviewManifest.copyWith(
557 logger: logger,
558 assets: assets,
559 fonts: fonts,
560 shaders: shaders,
561 deferredComponents: deferredComponents,
562 );
563 }
564
565 Future<void> _populatePreviewPubspec({required FlutterProject rootProject}) async {
566 final FlutterProject widgetPreviewScaffoldProject = rootProject.widgetPreviewScaffoldProject;
567
568 // Overwrite the pubspec for the preview scaffold project to include assets
569 // from the root project.
570 widgetPreviewScaffoldProject.replacePubspec(
571 buildPubspec(
572 rootManifest: rootProject.manifest,
573 widgetPreviewManifest: widgetPreviewScaffoldProject.manifest,
574 ),
575 );
576
577 // Adds a path dependency on the parent project so previews can be
578 // imported directly into the preview scaffold.
579 const String pubAdd = 'add';
580 await pub.interactively(
581 <String>[
582 pubAdd,
583 if (offline) '--offline',
584 '--directory',
585 widgetPreviewScaffoldProject.directory.path,
586 // Ensure the path using POSIX separators, otherwise the "path_not_posix" check will fail.
587 '${rootProject.manifest.appName}:{"path":${rootProject.directory.path.replaceAll(r"\", "/")}}',
588 ],
589 context: PubContext.pubAdd,
590 command: pubAdd,
591 touchesPackageConfig: true,
592 );
593
594 // Adds a dependency on flutter_lints, which is referenced by the
595 // analysis_options.yaml generated by the 'app' template.
596 await pub.interactively(
597 <String>[
598 pubAdd,
599 if (offline) '--offline',
600 '--directory',
601 widgetPreviewScaffoldProject.directory.path,
602 'flutter_lints',
603 'stack_trace',
604 ],
605 context: PubContext.pubAdd,
606 command: pubAdd,
607 touchesPackageConfig: true,
608 );
609
610 // Generate package_config.json.
611 await pub.get(
612 context: PubContext.create,
613 project: widgetPreviewScaffoldProject,
614 offline: offline,
615 outputMode: PubOutputMode.summaryOnly,
616 );
617
618 maybeAddFlutterGenToPackageConfig(rootProject: rootProject);
619 _previewManifest.updatePubspecHash();
620 }
621
622 /// Manually adds an entry for package:flutter_gen to the preview scaffold's
623 /// package_config.json if the target project makes use of localization.
624 ///
625 /// The Flutter Tool does this when running a Flutter project with
626 /// localization instead of modifying the user's pubspec.yaml to depend on it
627 /// as a path dependency. Unfortunately, the preview scaffold still needs to
628 /// add it directly to its package_config.json as the generated package name
629 /// isn't actually flutter_gen, which pub doesn't really like, and using the
630 /// actual package name will break applications which import
631 /// package:flutter_gen.
632 @visibleForTesting
633 void maybeAddFlutterGenToPackageConfig({required FlutterProject rootProject}) {
634 // TODO(matanlurey): Remove this once flutter_gen is removed.
635 //
636 // This is actually incorrect logic; the presence of a `generate: true`
637 // does *NOT* mean that we need to add `flutter_gen` to the package config,
638 // and never did, but the name of the manifest field was labeled and
639 // described incorrectly.
640 //
641 // Tracking removal: https://github.com/flutter/flutter/issues/102983.
642 if (!rootProject.manifest.generateLocalizations) {
643 return;
644 }
645 final FlutterProject widgetPreviewScaffoldProject = rootProject.widgetPreviewScaffoldProject;
646 final File packageConfig = widgetPreviewScaffoldProject.packageConfig;
647 final String previewPackageConfigPath = packageConfig.path;
648 if (!packageConfig.existsSync()) {
649 throw StateError(
650 "Could not find preview project's package_config.json at "
651 '$previewPackageConfigPath',
652 );
653 }
654 final Map<String, Object?> packageConfigJson =
655 json.decode(packageConfig.readAsStringSync()) as Map<String, Object?>;
656 (packageConfigJson['packages'] as List<dynamic>?)!.cast<Map<String, String>>().add(
657 flutterGenPackageConfigEntry,
658 );
659 packageConfig.writeAsStringSync(json.encode(packageConfigJson));
660 logger.printStatus('Added flutter_gen dependency to $previewPackageConfigPath');
661 }
662}
663
664final class WidgetPreviewCleanCommand extends WidgetPreviewSubCommandBase {
665 WidgetPreviewCleanCommand({required this.fs, required this.logger, required this.projectFactory});
666
667 @override
668 String get description => 'Cleans up widget preview state.';
669
670 @override
671 String get name => 'clean';
672
673 @override
674 final FileSystem fs;
675
676 @override
677 final Logger logger;
678
679 @override
680 final FlutterProjectFactory projectFactory;
681
682 @override
683 Future<FlutterCommandResult> runCommand() async {
684 final Directory widgetPreviewScaffold = getRootProject().widgetPreviewScaffold;
685 if (widgetPreviewScaffold.existsSync()) {
686 final String scaffoldPath = widgetPreviewScaffold.path;
687 logger.printStatus('Deleting widget preview scaffold at $scaffoldPath.');
688 widgetPreviewScaffold.deleteSync(recursive: true);
689 } else {
690 logger.printStatus('Nothing to clean up.');
691 }
692 return FlutterCommandResult.success();
693 }
694}
695

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com