// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:meta/meta.dart'; import '../base/deferred_component.dart'; import '../base/file_system.dart'; import '../base/logger.dart'; import '../convert.dart'; import '../dart/pub.dart'; import '../flutter_manifest.dart'; import '../project.dart'; import 'preview_manifest.dart'; /// Builds and manages the pubspec for the widget preview scaffold class PreviewPubspecBuilder { const PreviewPubspecBuilder({ required this.logger, required this.verbose, required this.offline, required this.rootProject, required this.previewManifest, }); final Logger logger; final bool verbose; /// Set to true if pub should operate in offline mode. final bool offline; /// The Flutter project that contains widget previews. final FlutterProject rootProject; /// Details about the current state of the widget preview scaffold project. final PreviewManifest previewManifest; /// Adds dependencies on: /// - dtd, which is used to connect to the Dart Tooling Daemon to establish communication /// with other developer tools. /// - flutter_lints, which is referenced by the analysis_options.yaml generated by the 'app' /// template. /// - google_fonts, which is used for the Roboto Mono font. /// - json_rpc_2, which is used to handle errors thrown by package:dtd /// - path, which is used to normalize and compare paths. /// - stack_trace, which is used to generate terse stack traces for displaying errors thrown /// by widgets being previewed. /// - url_launcher, which is used to open a browser to the preview documentation. /// - web, which is used to access query parameters provided by the IDE. /// - webview_flutter, webview_flutter_web, which is used to embed DevTools in the previewer. static const _kWidgetPreviewScaffoldDeps = [ 'dtd', 'flutter_lints', 'google_fonts', 'json_rpc_2', 'path', 'stack_trace', 'url_launcher', 'web', 'webview_flutter', 'webview_flutter_web', ]; /// Maps asset URIs to absolute paths for the widget preview project to /// include. @visibleForTesting Uri transformAssetUri(Uri uri) { // Assets provided by packages always start with 'packages' and do not // require their URIs to be updated. if (uri.path.startsWith('packages')) { return uri; } // Otherwise, the asset is contained within the root project and needs // to be referenced from the widget preview scaffold project's pubspec. final Directory rootProjectDir = rootProject.directory; final FileSystem fs = rootProjectDir.fileSystem; return Uri(path: fs.path.join(rootProjectDir.absolute.path, uri.path)); } @visibleForTesting AssetsEntry transformAssetsEntry(AssetsEntry asset) { return AssetsEntry( uri: transformAssetUri(asset.uri), flavors: asset.flavors, platforms: asset.platforms, transformers: asset.transformers, ); } @visibleForTesting DeferredComponent transformDeferredComponent(DeferredComponent component) { return DeferredComponent( name: component.name, // TODO(bkonyi): verify these library paths are always package: paths from the parent project. libraries: component.libraries, assets: component.assets.map(transformAssetsEntry).toList(), ); } PubOutputMode get _outputMode => verbose ? PubOutputMode.all : PubOutputMode.failuresOnly; Future populatePreviewPubspec({ required FlutterProject rootProject, String? updatedPubspecPath, }) async { final FlutterProject widgetPreviewScaffoldProject = rootProject.widgetPreviewScaffoldProject; // Overwrite the pubspec for the preview scaffold project to include assets // from the root project. Dependencies are removed as part of this operation // and they need to be added back below. widgetPreviewScaffoldProject.replacePubspec( buildPubspec( rootProject: rootProject, widgetPreviewManifest: widgetPreviewScaffoldProject.manifest, ), ); // Adds a path dependency on the parent project so previews can be // imported directly into the preview scaffold. const pubAdd = 'add'; final workspacePackages = { for (final FlutterProject project in [ rootProject, ...rootProject.workspaceProjects, ]) // Don't try and depend on unnamed projects. if (project.manifest.appName.isNotEmpty) // Use `json.encode` to handle escapes correctly. project.manifest.appName: json.encode({ 'path': widgetPreviewScaffoldProject.directory.fileSystem.path.absolute( project.directory.path, ), }), }; await pub.interactively( [ pubAdd, if (offline) '--offline', '--directory', widgetPreviewScaffoldProject.directory.path, // Ensure the path using POSIX separators, otherwise the "path_not_posix" check will fail. for (final MapEntry(:String key, :String value) in workspacePackages.entries) ...[ '$key:$value', // Add dependency overrides to handle "hosted" dependencies on other projects within the // workspace. These dependencies take the form of "my_workspace_project: " in the // pubspec's dependency list. See https://github.com/flutter/flutter/issues/176018. 'override:$key:$value', ], ], context: PubContext.pubAdd, command: pubAdd, touchesPackageConfig: true, outputMode: _outputMode, ); // Adds dependencies required by the widget preview scaffolding. await pub.interactively( [ pubAdd, if (offline) '--offline', '--directory', widgetPreviewScaffoldProject.directory.path, ..._kWidgetPreviewScaffoldDeps, ], context: PubContext.pubAdd, command: pubAdd, touchesPackageConfig: true, outputMode: _outputMode, ); await generatePackageConfig(widgetPreviewScaffoldProject: widgetPreviewScaffoldProject); previewManifest.updatePubspecHash(updatedPubspecPath: updatedPubspecPath); } /// Generates `widget_preview_scaffold/.dart_tool/package_config.json`. Future generatePackageConfig({required FlutterProject widgetPreviewScaffoldProject}) async { // Generate package_config.json. await pub.get( context: PubContext.create, project: widgetPreviewScaffoldProject, offline: offline, outputMode: _outputMode, ); } void onPubspecChangeDetected(String path) { // TODO(bkonyi): trigger hot reload or restart? logger.printStatus('Changes to $path detected.'); populatePreviewPubspec(rootProject: rootProject, updatedPubspecPath: path); } @visibleForTesting FlutterManifest buildPubspec({ required FlutterProject rootProject, required FlutterManifest widgetPreviewManifest, }) { final deferredComponents = [ ...?rootProject.manifest.deferredComponents?.map(transformDeferredComponent), for (final FlutterProject project in rootProject.workspaceProjects) ...?project.manifest.deferredComponents?.map(transformDeferredComponent), ]; // Copy the manifest with dependencies removed to handle situations where a package or // workspace name has changed. We'll re-add them later. return widgetPreviewManifest.copyWith( logger: logger, deferredComponents: deferredComponents, removeDependencies: true, ); } }