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 | |
5 | import 'package:args/args.dart'; |
6 | import 'package:meta/meta.dart'; |
7 | import 'package:package_config/package_config.dart'; |
8 | |
9 | import '../base/common.dart'; |
10 | import '../base/deferred_component.dart'; |
11 | import '../base/file_system.dart'; |
12 | import '../base/logger.dart'; |
13 | import '../base/os.dart'; |
14 | import '../base/platform.dart'; |
15 | import '../base/process.dart'; |
16 | import '../build_info.dart'; |
17 | import '../bundle.dart' as bundle; |
18 | import '../cache.dart'; |
19 | import '../convert.dart'; |
20 | import '../dart/pub.dart'; |
21 | import '../device.dart'; |
22 | import '../flutter_manifest.dart'; |
23 | import '../linux/build_linux.dart'; |
24 | import '../macos/build_macos.dart'; |
25 | import '../project.dart'; |
26 | import '../runner/flutter_command.dart'; |
27 | import '../widget_preview/preview_code_generator.dart'; |
28 | import '../widget_preview/preview_detector.dart'; |
29 | import '../widget_preview/preview_manifest.dart'; |
30 | import '../windows/build_windows.dart'; |
31 | import 'create_base.dart'; |
32 | import 'daemon.dart'; |
33 | |
34 | class 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 | |
80 | abstract 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 | |
111 | final 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 | |
664 | final 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 |
Definitions
- WidgetPreviewCommand
- WidgetPreviewCommand
- description
- name
- category
- hidden
- runCommand
- WidgetPreviewSubCommandBase
- fs
- logger
- projectFactory
- getRootProject
- validateFlutterProjectForPreview
- WidgetPreviewStartCommand
- WidgetPreviewStartCommand
- requiredArtifacts
- description
- name
- isWeb
- runCommand
- onChangeDetected
- onPubspecChangeDetected
- initialBuild
- prebuiltApplicationBinaryPath
- runPreviewEnvironment
- transformAssetUri
- transformAssetsEntry
- transformFontAsset
- transformDeferredComponent
- buildPubspec
- _populatePreviewPubspec
- maybeAddFlutterGenToPackageConfig
- WidgetPreviewCleanCommand
- WidgetPreviewCleanCommand
- description
- name
Learn more about Flutter for embedded and desktop on industrialflutter.com