[ Widget Previews ] Add support for Pub Workspaces (#171538)
Updates the widget previewer to properly handle previews defined across
a workspace. This is effectively equivalent to displaying previews from
multiple individual projects, which required a fairly significant rework
to properly support:
- resolving asset paths to the right project
- loading assets across projects in the workspace
- tracking changes to pubspec.yaml files across the workspace
While making this change, it became apparent that we don't need to
explicitly list the assets from the various projects explicitly in the
scaffold's pubspec.yaml as they're automatically pulled in by adding
those project's as dependencies and using the `package/$ASSET_PATH`
format to load the assets from the scaffold. This allowed for the
removal of a significant amount of logic related to building the
scaffold's pubspec.
Fixes https://github.com/flutter/flutter/issues/169268
diff --git a/packages/flutter_tools/lib/src/commands/widget_preview.dart b/packages/flutter_tools/lib/src/commands/widget_preview.dart
index 403097d..5d719b0 100644
--- a/packages/flutter_tools/lib/src/commands/widget_preview.dart
+++ b/packages/flutter_tools/lib/src/commands/widget_preview.dart
@@ -281,12 +281,6 @@
'widget preview scaffold.',
);
}
- // TODO(matanlurey): Remove this comment once flutter_gen is removed.
- //
- // Tracking removal: https://github.com/flutter/flutter/issues/102983.
- //
- // Populate the pubspec after the initial build to avoid blowing away the package_config.json
- // which may have manual changes for flutter_gen support.
await _previewPubspecBuilder.populatePreviewPubspec(rootProject: rootProject);
}
diff --git a/packages/flutter_tools/lib/src/flutter_manifest.dart b/packages/flutter_tools/lib/src/flutter_manifest.dart
index eb2d81b..fd01d04 100644
--- a/packages/flutter_tools/lib/src/flutter_manifest.dart
+++ b/packages/flutter_tools/lib/src/flutter_manifest.dart
@@ -5,6 +5,7 @@
/// @docImport 'localizations/gen_l10n.dart';
library;
+import 'package:crypto/crypto.dart';
import 'package:meta/meta.dart';
import 'package:pub_semver/pub_semver.dart';
import 'package:yaml/yaml.dart';
@@ -79,9 +80,16 @@
List<Font>? fonts,
List<Uri>? shaders,
List<DeferredComponent>? deferredComponents,
+ bool removeDependencies = false,
}) {
final FlutterManifest copy = FlutterManifest._(logger: _logger);
copy._descriptor = <String, Object?>{..._descriptor};
+ if (removeDependencies) {
+ // Remove the non-Flutter SDK dependencies if they're going to be added back later.
+ copy._descriptor['dependencies'] = YamlMap.wrap(<String, Object?>{
+ 'flutter': <String, Object?>{'sdk': 'flutter'},
+ });
+ }
copy._flutterDescriptor = <String, Object?>{..._flutterDescriptor};
if (assets != null && assets.isNotEmpty) {
@@ -265,6 +273,11 @@
};
}
+ /// Returns the MD5 hash of the manifest contents.
+ String computeMD5Hash() {
+ return md5.convert(toYaml().toString().codeUnits).toString();
+ }
+
/// Returns the deferred components configuration if declared. Returns
/// null if no deferred components are declared.
late final List<DeferredComponent>? deferredComponents = computeDeferredComponents();
diff --git a/packages/flutter_tools/lib/src/widget_preview/dependency_graph.dart b/packages/flutter_tools/lib/src/widget_preview/dependency_graph.dart
index 14f8933..216695b 100644
--- a/packages/flutter_tools/lib/src/widget_preview/dependency_graph.dart
+++ b/packages/flutter_tools/lib/src/widget_preview/dependency_graph.dart
@@ -32,6 +32,11 @@
/// Visitor which detects previews and extracts [PreviewDetails] for later code
/// generation.
class _PreviewVisitor extends RecursiveAstVisitor<void> {
+ _PreviewVisitor({required LibraryElement2 lib})
+ : packageName = lib.uri.scheme == 'package' ? lib.uri.pathSegments.first : null;
+
+ late final String? packageName;
+
final List<PreviewDetails> previewEntries = <PreviewDetails>[];
FunctionDeclaration? _currentFunction;
@@ -78,6 +83,7 @@
if (_currentFunction != null) {
final NamedType returnType = _currentFunction!.returnType! as NamedType;
_currentPreview = PreviewDetails(
+ packageName: packageName,
functionName: _currentFunction!.name.toString(),
isBuilder: returnType.name2.isWidgetBuilder,
);
@@ -85,6 +91,7 @@
final SimpleIdentifier returnType = _currentConstructor!.returnType as SimpleIdentifier;
final Token? name = _currentConstructor!.name;
_currentPreview = PreviewDetails(
+ packageName: packageName,
functionName: '$returnType${name == null ? '' : '.$name'}',
isBuilder: false,
);
@@ -92,6 +99,7 @@
final NamedType returnType = _currentMethod!.returnType! as NamedType;
final ClassDeclaration parentClass = _currentMethod!.parent! as ClassDeclaration;
_currentPreview = PreviewDetails(
+ packageName: packageName,
functionName: '${parentClass.name}.${_currentMethod!.name}',
isBuilder: returnType.name2.isWidgetBuilder,
);
@@ -169,11 +177,11 @@
}
}
- /// Finds all previews defined in the compilation [units] and adds them to [previews].
- void findPreviews({required List<ResolvedUnitResult> units}) {
+ /// Finds all previews defined in the [lib] and adds them to [previews].
+ void findPreviews({required ResolvedLibraryResult lib}) {
// Iterate over the compilation unit's AST to find previews.
- final _PreviewVisitor visitor = _PreviewVisitor();
- for (final ResolvedUnitResult libUnit in units) {
+ final _PreviewVisitor visitor = _PreviewVisitor(lib: lib.element2);
+ for (final ResolvedUnitResult libUnit in lib.units) {
libUnit.unit.visitChildren(visitor);
}
previews
@@ -195,6 +203,11 @@
for (final ResolvedUnitResult unit in units) {
final LibraryFragment fragment = unit.libraryFragment;
for (final LibraryImport importedLib in fragment.libraryImports2) {
+ if (importedLib.importedLibrary2 == null) {
+ // This is an import for a file that's not analyzed (likely an import of a package from
+ // the pub-cache) and isn't necessary to track as part of the dependency graph.
+ continue;
+ }
final LibraryElement2 importedLibrary = importedLib.importedLibrary2!;
final LibraryPreviewNode result = graph.putIfAbsent(
importedLibrary.toPreviewPath(),
diff --git a/packages/flutter_tools/lib/src/widget_preview/preview_code_generator.dart b/packages/flutter_tools/lib/src/widget_preview/preview_code_generator.dart
index b62d5e5..2efd28e8 100644
--- a/packages/flutter_tools/lib/src/widget_preview/preview_code_generator.dart
+++ b/packages/flutter_tools/lib/src/widget_preview/preview_code_generator.dart
@@ -194,6 +194,8 @@
// TODO(bkonyi): try to display the preview name, even if the preview can't be displayed.
if (!libraryDetails.dependencyHasErrors &&
!libraryDetails.hasErrors) ...<String, cb.Expression>{
+ if (preview.packageName != null)
+ PreviewDetails.kPackageName: cb.literalString(preview.packageName!),
...?_generateCodeFromAnalyzerExpression(
allocator: allocator,
key: PreviewDetails.kName,
diff --git a/packages/flutter_tools/lib/src/widget_preview/preview_details.dart b/packages/flutter_tools/lib/src/widget_preview/preview_details.dart
index 1a4e3fe..bc6c60f 100644
--- a/packages/flutter_tools/lib/src/widget_preview/preview_details.dart
+++ b/packages/flutter_tools/lib/src/widget_preview/preview_details.dart
@@ -6,8 +6,9 @@
/// Contains details related to a single preview instance.
final class PreviewDetails {
- PreviewDetails({required this.functionName, required this.isBuilder});
+ PreviewDetails({required this.packageName, required this.functionName, required this.isBuilder});
+ static const String kPackageName = 'packageName';
static const String kName = 'name';
static const String kSize = 'size';
static const String kTextScaleFactor = 'textScaleFactor';
@@ -16,6 +17,15 @@
static const String kBrightness = 'brightness';
static const String kLocalizations = 'localizations';
+ /// The name of the package in which the preview was defined.
+ ///
+ /// For example, if this preview is defined in 'package:foo/src/bar.dart', this
+ /// will have the value 'foo'.
+ ///
+ /// This should only be null if the preview is defined in a file that's not
+ /// part of a Flutter library (e.g., is defined in a test).
+ final String? packageName;
+
/// The name of the function returning the preview.
final String functionName;
@@ -115,6 +125,7 @@
}
return other.runtimeType == runtimeType &&
other is PreviewDetails &&
+ other.packageName == packageName &&
other.functionName == functionName &&
other.isBuilder == isBuilder &&
other.size == size &&
@@ -127,13 +138,14 @@
@override
String toString() =>
- 'PreviewDetails(function: $functionName isBuilder: $isBuilder $kName: $name '
- '$kSize: $size $kTextScaleFactor: $textScaleFactor $kWrapper: $wrapper '
+ 'PreviewDetails(function: $functionName packageName: $packageName isBuilder: $isBuilder '
+ '$kName: $name $kSize: $size $kTextScaleFactor: $textScaleFactor $kWrapper: $wrapper '
'$kTheme: $theme $kBrightness: $_brightness $kLocalizations: $_localizations)';
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
int get hashCode => Object.hashAll(<Object?>[
+ packageName,
functionName,
isBuilder,
size,
diff --git a/packages/flutter_tools/lib/src/widget_preview/preview_detector.dart b/packages/flutter_tools/lib/src/widget_preview/preview_detector.dart
index 15d8214..f895a78 100644
--- a/packages/flutter_tools/lib/src/widget_preview/preview_detector.dart
+++ b/packages/flutter_tools/lib/src/widget_preview/preview_detector.dart
@@ -30,7 +30,7 @@
final FileSystem fs;
final Logger logger;
final void Function(PreviewDependencyGraph) onChangeDetected;
- final void Function() onPubspecChangeDetected;
+ final void Function(String path) onPubspecChangeDetected;
StreamSubscription<WatchEvent>? _fileWatcher;
final PreviewDetectorMutex _mutex = PreviewDetectorMutex();
@@ -79,7 +79,7 @@
// If the pubspec has changed, new dependencies or assets could have been added, requiring
// the preview scaffold's pubspec to be updated.
if (eventPath.isPubspec && !eventPath.doesContainDartTool) {
- onPubspecChangeDetected();
+ onPubspecChangeDetected(eventPath);
return;
}
// Only trigger a reload when changes to Dart sources are detected. We
@@ -99,7 +99,7 @@
// extension which may be worth using here.
// We need to notify the analyzer that this file has changed so it can reanalyze the file.
- final AnalysisContext context = collection.contexts.single;
+ final AnalysisContext context = collection.contextFor(eventPath);
final File file = fs.file(eventPath);
context.changeFile(file.path);
await context.applyPendingFileChanges();
@@ -152,45 +152,46 @@
Future<PreviewDependencyGraph> _findPreviewFunctions(FileSystemEntity entity) async {
final PreviewDependencyGraph updatedPreviews = PreviewDependencyGraph();
- final AnalysisContext context = collection.contexts.single;
logger.printStatus('Finding previews in ${entity.path}...');
- for (final String filePath in context.contextRoot.analyzedFiles()) {
- logger.printTrace('Checking file: $filePath');
- if (!filePath.isDartFile || !filePath.startsWith(entity.path)) {
- logger.printTrace('Skipping $filePath');
- continue;
- }
- SomeResolvedLibraryResult lib = await context.currentSession.getResolvedLibrary(filePath);
- // If filePath points to a file that's part of a library, retrieve its compilation unit first
- // in order to get the actual path to the library.
- if (lib is NotLibraryButPartResult) {
- final ResolvedUnitResult unit =
- (await context.currentSession.getResolvedUnit(filePath)) as ResolvedUnitResult;
- lib = await context.currentSession.getResolvedLibrary(
- unit.libraryElement2.firstFragment.source.fullName,
- );
- }
- if (lib is ResolvedLibraryResult) {
- final ResolvedLibraryResult resolvedLib = lib;
- final PreviewPath previewPath = lib.element2.toPreviewPath();
- // This library has already been processed.
- if (updatedPreviews.containsKey(previewPath)) {
+ for (final AnalysisContext context in collection.contexts) {
+ for (final String filePath in context.contextRoot.analyzedFiles()) {
+ logger.printTrace('Checking file: $filePath');
+ if (!filePath.isDartFile || !filePath.startsWith(entity.path)) {
+ logger.printTrace('Skipping $filePath');
continue;
}
+ SomeResolvedLibraryResult lib = await context.currentSession.getResolvedLibrary(filePath);
+ // If filePath points to a file that's part of a library, retrieve its compilation unit first
+ // in order to get the actual path to the library.
+ if (lib is NotLibraryButPartResult) {
+ final ResolvedUnitResult unit =
+ (await context.currentSession.getResolvedUnit(filePath)) as ResolvedUnitResult;
+ lib = await context.currentSession.getResolvedLibrary(
+ unit.libraryElement2.firstFragment.source.fullName,
+ );
+ }
+ if (lib is ResolvedLibraryResult) {
+ final ResolvedLibraryResult resolvedLib = lib;
+ final PreviewPath previewPath = lib.element2.toPreviewPath();
+ // This library has already been processed.
+ if (updatedPreviews.containsKey(previewPath)) {
+ continue;
+ }
- final LibraryPreviewNode previewsForLibrary = _dependencyGraph.putIfAbsent(
- previewPath,
- () => LibraryPreviewNode(library: resolvedLib.element2, logger: logger),
- );
+ final LibraryPreviewNode previewsForLibrary = _dependencyGraph.putIfAbsent(
+ previewPath,
+ () => LibraryPreviewNode(library: resolvedLib.element2, logger: logger),
+ );
- previewsForLibrary.updateDependencyGraph(graph: _dependencyGraph, units: lib.units);
- updatedPreviews[previewPath] = previewsForLibrary;
+ previewsForLibrary.updateDependencyGraph(graph: _dependencyGraph, units: lib.units);
+ updatedPreviews[previewPath] = previewsForLibrary;
- // Check for errors in the library.
- await previewsForLibrary.populateErrors(context: context);
+ // Check for errors in the library.
+ await previewsForLibrary.populateErrors(context: context);
- // Iterate over each compilation unit's AST to find previews.
- previewsForLibrary.findPreviews(units: lib.units);
+ // Iterate over each library's AST to find previews.
+ previewsForLibrary.findPreviews(lib: lib);
+ }
}
}
final int previewCount = updatedPreviews.values.fold<int>(
diff --git a/packages/flutter_tools/lib/src/widget_preview/preview_manifest.dart b/packages/flutter_tools/lib/src/widget_preview/preview_manifest.dart
index 727a72c..f14fc52 100644
--- a/packages/flutter_tools/lib/src/widget_preview/preview_manifest.dart
+++ b/packages/flutter_tools/lib/src/widget_preview/preview_manifest.dart
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-import 'package:crypto/crypto.dart';
+import 'package:collection/equality.dart';
import 'package:meta/meta.dart';
import '../base/file_system.dart';
@@ -23,10 +23,10 @@
});
static const String previewManifestPath = 'preview_manifest.json';
- static final Version previewManifestVersion = Version(0, 0, 1);
+ static final Version previewManifestVersion = Version(0, 0, 2);
static const String kManifestVersion = 'version';
static const String kSdkVersion = 'sdk-version';
- static const String kPubspecHash = 'pubspec-hash';
+ static const String kPubspecHashes = 'pubspec-hashes';
final Logger logger;
final FlutterProject rootProject;
@@ -52,7 +52,7 @@
final PreviewManifestContents manifestContents = <String, Object?>{
kManifestVersion: previewManifestVersion.toString(),
kSdkVersion: cache.dartSdkVersion,
- kPubspecHash: _calculatePubspecHash(),
+ kPubspecHashes: _calculatePubspecHashes(),
};
_updateManifest(manifestContents);
}
@@ -61,8 +61,30 @@
_manifest.writeAsStringSync(json.encode(contents));
}
- String _calculatePubspecHash() {
- return md5.convert(rootProject.manifest.toYaml().toString().codeUnits).toString();
+ Map<String, String> _calculatePubspecHashes({String? updatedPubspecPath}) {
+ if (updatedPubspecPath != null) {
+ final PreviewManifestContents? manifest = _tryLoadManifest();
+ if (manifest != null) {
+ final FlutterProject project = <FlutterProject>[
+ rootProject,
+ ...rootProject.workspaceProjects,
+ ].firstWhere(
+ (FlutterProject project) => project.pubspecFile.absolute.path == updatedPubspecPath,
+ );
+ final Map<String, String> pubspecHashes =
+ (manifest[kPubspecHashes]! as Map<String, Object?>).cast<String, String>();
+ pubspecHashes[updatedPubspecPath] = project.manifest.computeMD5Hash();
+ return pubspecHashes;
+ }
+ }
+
+ return <String, String>{
+ for (final FlutterProject project in <FlutterProject>[
+ rootProject,
+ ...rootProject.workspaceProjects,
+ ])
+ project.pubspecFile.absolute.path: project.manifest.computeMD5Hash(),
+ };
}
bool shouldGenerateProject() {
@@ -109,19 +131,21 @@
bool shouldRegeneratePubspec() {
final PreviewManifestContents manifest = _tryLoadManifest()!;
- if (!manifest.containsKey(kPubspecHash)) {
+ if (!manifest.containsKey(kPubspecHashes)) {
logger.printWarning(
'The Widget Preview Scaffold manifest does not include the last known state of the root '
"project's pubspec.yaml.",
);
return true;
}
- return manifest[kPubspecHash] != _calculatePubspecHash();
+ final Map<String, String> pubspecHashes =
+ (manifest[kPubspecHashes]! as Map<String, Object?>).cast<String, String>();
+ return !const MapEquality<String, String>().equals(pubspecHashes, _calculatePubspecHashes());
}
- void updatePubspecHash() {
+ void updatePubspecHash({String? updatedPubspecPath}) {
final PreviewManifestContents manifest = _tryLoadManifest()!;
- manifest[kPubspecHash] = _calculatePubspecHash();
+ manifest[kPubspecHashes] = _calculatePubspecHashes(updatedPubspecPath: updatedPubspecPath);
_updateManifest(manifest);
}
diff --git a/packages/flutter_tools/lib/src/widget_preview/preview_pubspec_builder.dart b/packages/flutter_tools/lib/src/widget_preview/preview_pubspec_builder.dart
index f48a6d3..9f81c94 100644
--- a/packages/flutter_tools/lib/src/widget_preview/preview_pubspec_builder.dart
+++ b/packages/flutter_tools/lib/src/widget_preview/preview_pubspec_builder.dart
@@ -75,11 +75,6 @@
}
@visibleForTesting
- static FontAsset transformFontAsset(FontAsset asset) {
- return FontAsset(transformAssetUri(asset.assetUri), weight: asset.weight, style: asset.style);
- }
-
- @visibleForTesting
static DeferredComponent transformDeferredComponent(DeferredComponent component) {
return DeferredComponent(
name: component.name,
@@ -89,14 +84,18 @@
);
}
- Future<void> populatePreviewPubspec({required FlutterProject rootProject}) async {
+ Future<void> 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.
+ // from the root project. Dependencies are removed as part of this operation
+ // and they need to be added back below.
widgetPreviewScaffoldProject.replacePubspec(
buildPubspec(
- rootManifest: rootProject.manifest,
+ rootProject: rootProject,
widgetPreviewManifest: widgetPreviewScaffoldProject.manifest,
),
);
@@ -104,11 +103,19 @@
// Adds a path dependency on the parent project so previews can be
// imported directly into the preview scaffold.
const String pubAdd = 'add';
- // Use `json.encode` to handle escapes correctly.
- final String pathDescriptor = json.encode(<String, Object?>{
- // `pub add` interprets relative paths relative to the current directory.
- 'path': rootProject.directory.fileSystem.path.relative(rootProject.directory.path),
- });
+ final Map<String, String> workspacePackages = <String, String>{
+ for (final FlutterProject project in <FlutterProject>[
+ rootProject,
+ ...rootProject.workspaceProjects,
+ ])
+ // Use `json.encode` to handle escapes correctly.
+ project.manifest.appName: json.encode(<String, Object?>{
+ // `pub add` interprets relative paths relative to the current directory.
+ 'path': widgetPreviewScaffoldProject.directory.fileSystem.path.relative(
+ project.directory.path,
+ ),
+ }),
+ };
final PubOutputMode outputMode = verbose ? PubOutputMode.all : PubOutputMode.failuresOnly;
await pub.interactively(
@@ -118,7 +125,9 @@
'--directory',
widgetPreviewScaffoldProject.directory.path,
// Ensure the path using POSIX separators, otherwise the "path_not_posix" check will fail.
- '${rootProject.manifest.appName}:$pathDescriptor',
+ for (final MapEntry<String, String>(:String key, :String value)
+ in workspacePackages.entries)
+ '$key:$value',
],
context: PubContext.pubAdd,
command: pubAdd,
@@ -149,40 +158,32 @@
outputMode: outputMode,
);
- previewManifest.updatePubspecHash();
+ previewManifest.updatePubspecHash(updatedPubspecPath: updatedPubspecPath);
}
- void onPubspecChangeDetected() {
+ void onPubspecChangeDetected(String path) {
// TODO(bkonyi): trigger hot reload or restart?
- logger.printStatus('Changes to pubspec.yaml detected.');
- populatePreviewPubspec(rootProject: rootProject);
+ logger.printStatus('Changes to $path detected.');
+ populatePreviewPubspec(rootProject: rootProject, updatedPubspecPath: path);
}
@visibleForTesting
FlutterManifest buildPubspec({
- required FlutterManifest rootManifest,
+ required FlutterProject rootProject,
required FlutterManifest widgetPreviewManifest,
}) {
- final List<AssetsEntry> assets = rootManifest.assets.map(transformAssetsEntry).toList();
-
- final List<Font> fonts = <Font>[
- ...widgetPreviewManifest.fonts,
- ...rootManifest.fonts.map((Font font) {
- return Font(font.familyName, font.fontAssets.map(transformFontAsset).toList());
- }),
+ final List<DeferredComponent> deferredComponents = <DeferredComponent>[
+ ...?rootProject.manifest.deferredComponents?.map(transformDeferredComponent),
+ for (final FlutterProject project in rootProject.workspaceProjects)
+ ...?project.manifest.deferredComponents?.map(transformDeferredComponent),
];
- final List<Uri> shaders = rootManifest.shaders.map(transformAssetUri).toList();
-
- final List<DeferredComponent>? deferredComponents =
- rootManifest.deferredComponents?.map(transformDeferredComponent).toList();
-
+ // 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,
- assets: assets,
- fonts: fonts,
- shaders: shaders,
deferredComponents: deferredComponents,
+ removeDependencies: true,
);
}
}
diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml
index 5eec42a..7d1767b 100644
--- a/packages/flutter_tools/pubspec.yaml
+++ b/packages/flutter_tools/pubspec.yaml
@@ -15,6 +15,7 @@
dds: 5.0.4
dwds: 24.3.11
code_builder: 4.10.1
+ collection: 1.19.1
completion: 1.0.1
coverage: 1.14.1
crypto: 3.0.6
@@ -114,7 +115,6 @@
yaml_edit: 2.2.2
dev_dependencies:
- collection: 1.19.1
file_testing: 3.0.2
checked_yaml: 2.0.4
@@ -126,4 +126,4 @@
dartdoc:
# Exclude this package from the hosted API docs.
nodoc: true
-# PUBSPEC CHECKSUM: t46i0p
+# PUBSPEC CHECKSUM: e37mug
diff --git a/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/widget_preview.dart.tmpl b/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/widget_preview.dart.tmpl
index b063e62..bd55c12 100644
--- a/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/widget_preview.dart.tmpl
+++ b/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/widget_preview.dart.tmpl
@@ -21,6 +21,7 @@
/// properties.
const WidgetPreview({
required this.builder,
+ this.packageName,
this.name,
this.size,
this.textScaleFactor,
@@ -29,6 +30,12 @@
this.localizations,
});
+ /// The name of the package in which a preview was defined.
+ ///
+ /// For example, if a preview is defined in 'package:foo/src/bar.dart', this
+ /// will have the value 'foo'.
+ final String? packageName;
+
/// A description to be displayed alongside the preview.
///
/// If not provided, no name will be associated with the preview.
diff --git a/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/widget_preview_rendering.dart.tmpl b/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/widget_preview_rendering.dart.tmpl
index 42018bf..1bb327e 100644
--- a/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/widget_preview_rendering.dart.tmpl
+++ b/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/widget_preview_rendering.dart.tmpl
@@ -330,6 +330,21 @@
child: preview,
);
+ // Override the asset resolution behavior to automatically insert
+ // 'packages/$packageName/` in front of non-package paths as some previews
+ // may reference assets that are within the current project and wouldn't
+ // normally require a package specifier.
+ // TODO(bkonyi): this doesn't modify the behavior of asset loading logic in
+ // the engine implementation. This means that any asset loading done by
+ // APIs provided in dart:ui won't work correctly for non-package asset
+ // paths (e.g., shaders loaded by `FragmentProgram.fromAsset()`).
+ //
+ // See https://github.com/flutter/flutter/issues/171284
+ preview = DefaultAssetBundle(
+ bundle: PreviewAssetBundle(packageName: widget.preview.packageName),
+ child: preview,
+ );
+
preview = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -683,12 +698,28 @@
}
/// Custom [AssetBundle] used to map original asset paths from the parent
-/// project to those in the preview project.
+/// projects to those in the preview project.
class PreviewAssetBundle extends PlatformAssetBundle {
+ PreviewAssetBundle({required this.packageName});
+
+ /// The name of the package in which a preview was defined.
+ ///
+ /// For example, if a preview is defined in 'package:foo/src/bar.dart', this
+ /// will have the value 'foo'.
+ ///
+ /// This should only be null if the preview is defined in a file that's not
+ /// part of a Flutter library (e.g., is defined in a test).
+ // TODO(bkonyi): verify what the behavior should be in this scenario.
+ final String? packageName;
+
// Assets shipped via package dependencies have paths that start with
// 'packages'.
static const String _kPackagesPrefix = 'packages';
+ // TODO(bkonyi): when loading an invalid asset path that doesn't start with
+ // 'packages', this throws a FlutterError referencing the modified key
+ // instead of the original. We should catch the error and rethrow one with
+ // the original key in the error message.
@override
Future<ByteData> load(String key) {
// These assets are always present or are shipped via a package and aren't
@@ -697,12 +728,13 @@
if (key == 'AssetManifest.bin' ||
key == 'AssetManifest.json' ||
key == 'FontManifest.json' ||
- key.startsWith(_kPackagesPrefix)) {
+ key.startsWith(_kPackagesPrefix) ||
+ packageName == null) {
return super.load(key);
}
- // Other assets are from the parent project. Map their keys to those found
- // in the pubspec.yaml of the preview envirnment.
- return super.load('../../$key');
+ // Other assets are from the parent project. Map their keys to package
+ // paths corresponding to the package containing the preview.
+ return super.load(_toPackagePath(key));
}
@override
@@ -712,9 +744,11 @@
return ImmutableBuffer.fromUint8List(Uint8List.sublistView(bytes));
}
return await ImmutableBuffer.fromAsset(
- key.startsWith(_kPackagesPrefix) ? key : '../../$key',
+ key.startsWith(_kPackagesPrefix) ? key : _toPackagePath(key),
);
}
+
+ String _toPackagePath(String key) => '$_kPackagesPrefix/$packageName/$key';
}
/// Main entrypoint for the widget previewer.
@@ -875,16 +909,13 @@
debugShowCheckedModeBanner: false,
home: Material(
color: Colors.transparent,
- child: DefaultAssetBundle(
- bundle: PreviewAssetBundle(),
- child: Stack(
- children: [
- // Display the previewer
- _displayPreviewer(previewView),
- // Display the layout toggle buttons
- _displayToggleLayoutButtons(),
- ],
- ),
+ child: Stack(
+ children: [
+ // Display the previewer
+ _displayPreviewer(previewView),
+ // Display the layout toggle buttons
+ _displayToggleLayoutButtons(),
+ ],
),
),
);
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_code_generator_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_code_generator_test.dart
index 438e1be..4cac43d 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_code_generator_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_code_generator_test.dart
@@ -174,7 +174,7 @@
fs: fs,
logger: logger,
onChangeDetected: (_) {},
- onPubspecChangeDetected: () {},
+ onPubspecChangeDetected: (String path) {},
);
codeGenerator = PreviewCodeGenerator(widgetPreviewScaffoldProject: project, fs: fs);
final Pub pub = Pub.test(
@@ -229,13 +229,21 @@
import 'package:flutter/material.dart' as _i10;
List<_i1.WidgetPreview> previews() => [
- _i1.WidgetPreview(builder: () => _i2.preview()),
- _i1.WidgetPreview(builder: () => _i3.barPreview1()),
_i1.WidgetPreview(
+ packageName: 'foo_project',
+ builder: () => _i2.preview(),
+ ),
+ _i1.WidgetPreview(
+ packageName: 'foo_project',
+ builder: () => _i3.barPreview1(),
+ ),
+ _i1.WidgetPreview(
+ packageName: 'foo_project',
brightness: _i4.brightnessConstant,
builder: () => _i3.barPreview2(),
),
_i1.WidgetPreview(
+ packageName: 'foo_project',
name: 'Foo',
size: const _i5.Size(
123,
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_detector_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_detector/preview_detector_test.dart
similarity index 85%
rename from packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_detector_test.dart
rename to packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_detector/preview_detector_test.dart
index 0ea61af..af9d7db 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_detector_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_detector/preview_detector_test.dart
@@ -7,27 +7,21 @@
import 'package:flutter_tools/src/widget_preview/preview_detector.dart';
import 'package:test/test.dart';
-import '../../../src/common.dart';
-import '../../../src/context.dart';
-import 'utils/preview_details_matcher.dart';
-import 'utils/preview_detector_test_utils.dart';
-import 'utils/preview_project.dart';
+import '../../../../src/common.dart';
+import '../../../../src/context.dart';
+import '../utils/preview_details_matcher.dart';
+import '../utils/preview_detector_test_utils.dart';
+import '../utils/preview_project.dart';
// Note: this test isn't under the general.shard since tests under that directory
// have a 2000ms time out and these tests write to the real file system and watch
// directories for changes. This can be slow on heavily loaded machines and cause
// flaky failures.
-void populatePubspec(Directory projectRoot, String contents) {
- projectRoot.childFile('pubspec.yaml')
- ..createSync(recursive: true)
- ..writeAsStringSync(contents);
-}
-
/// Creates a project with files containing previews that attempt to use as many widget preview
/// properties as possible.
class BasicProjectWithExhaustivePreviews extends WidgetPreviewProject {
- BasicProjectWithExhaustivePreviews({
+ BasicProjectWithExhaustivePreviews._({
required super.projectRoot,
required List<String> pathsWithPreviews,
required List<String> pathsWithoutPreviews,
@@ -43,6 +37,19 @@
}
initialSources.forEach(writeFile);
}
+ static Future<BasicProjectWithExhaustivePreviews> create({
+ required Directory projectRoot,
+ required List<String> pathsWithPreviews,
+ required List<String> pathsWithoutPreviews,
+ }) async {
+ final BasicProjectWithExhaustivePreviews project = BasicProjectWithExhaustivePreviews._(
+ projectRoot: projectRoot,
+ pathsWithPreviews: pathsWithPreviews,
+ pathsWithoutPreviews: pathsWithoutPreviews,
+ );
+ await project.initializePubspec();
+ return project;
+ }
Map<PreviewPath, List<PreviewDetailsMatcher>> get matcherMapping =>
<PreviewPath, List<PreviewDetailsMatcher>>{
@@ -100,10 +107,21 @@
throw UnimplementedError('Not supported for $BasicProjectWithExhaustivePreviews');
}
- final List<PreviewDetailsMatcher> _expectedPreviewDetails = <PreviewDetailsMatcher>[
- PreviewDetailsMatcher(functionName: 'previews', isBuilder: false, name: 'Top-level preview'),
- PreviewDetailsMatcher(functionName: 'builderPreview', isBuilder: true, name: 'Builder preview'),
+ late final List<PreviewDetailsMatcher> _expectedPreviewDetails = <PreviewDetailsMatcher>[
PreviewDetailsMatcher(
+ packageName: packageName,
+ functionName: 'previews',
+ isBuilder: false,
+ name: 'Top-level preview',
+ ),
+ PreviewDetailsMatcher(
+ packageName: packageName,
+ functionName: 'builderPreview',
+ isBuilder: true,
+ name: 'Builder preview',
+ ),
+ PreviewDetailsMatcher(
+ packageName: packageName,
functionName: 'attributesPreview',
isBuilder: false,
nameSymbol: 'kAttributesPreview',
@@ -115,16 +133,19 @@
localizations: 'localizations',
),
PreviewDetailsMatcher(
+ packageName: packageName,
functionName: 'MyWidget.preview',
isBuilder: false,
name: 'Constructor preview',
),
PreviewDetailsMatcher(
+ packageName: packageName,
functionName: 'MyWidget.factoryPreview',
isBuilder: false,
name: 'Factory constructor preview',
),
PreviewDetailsMatcher(
+ packageName: packageName,
functionName: 'MyWidget.previewStatic',
isBuilder: false,
name: 'Static preview',
@@ -207,16 +228,6 @@
''';
}
-extension on PreviewDependencyGraph {
- PreviewDependencyGraph get nodesWithPreviews {
- return PreviewDependencyGraph.fromEntries(
- entries.where(
- (MapEntry<PreviewPath, LibraryPreviewNode> element) => element.value.previews.isNotEmpty,
- ),
- );
- }
-}
-
void main() {
initializeTestPreviewDetectorState();
group('$PreviewDetector', () {
@@ -235,7 +246,7 @@
});
testUsingContext('can detect previews in existing files', () async {
- project = BasicProjectWithExhaustivePreviews(
+ project = await BasicProjectWithExhaustivePreviews.create(
projectRoot: previewDetector.projectRoot,
pathsWithPreviews: <String>[
'foo.dart',
@@ -249,7 +260,7 @@
testUsingContext('can detect previews in updated files', () async {
// Create two files with existing previews and one without.
- project = BasicProjectWithExhaustivePreviews(
+ project = await BasicProjectWithExhaustivePreviews.create(
projectRoot: previewDetector.projectRoot,
pathsWithPreviews: <String>[
'foo.dart',
@@ -282,7 +293,7 @@
});
testUsingContext('can detect previews in newly added files', () async {
- project = BasicProjectWithExhaustivePreviews(
+ project = await BasicProjectWithExhaustivePreviews.create(
projectRoot: previewDetector.projectRoot,
pathsWithPreviews: <String>[],
pathsWithoutPreviews: <String>[],
@@ -305,17 +316,18 @@
});
testUsingContext('can detect previews in existing libraries with parts', () async {
- project = BasicProjectWithExhaustivePreviews(
- projectRoot: previewDetector.projectRoot,
- pathsWithPreviews: <String>[],
- pathsWithoutPreviews: <String>[],
- )..addLibraryWithPartsContainingPreviews(path: 'foo.dart');
+ project = await BasicProjectWithExhaustivePreviews.create(
+ projectRoot: previewDetector.projectRoot,
+ pathsWithPreviews: <String>[],
+ pathsWithoutPreviews: <String>[],
+ )
+ ..addLibraryWithPartsContainingPreviews(path: 'foo.dart');
final PreviewDependencyGraph mapping = await previewDetector.initialize();
expect(mapping.nodesWithPreviews.keys, unorderedMatches(project.librariesWithPreviews));
});
testUsingContext('can detect previews in newly added libraries with parts', () async {
- project = BasicProjectWithExhaustivePreviews(
+ project = await BasicProjectWithExhaustivePreviews.create(
projectRoot: previewDetector.projectRoot,
pathsWithPreviews: <String>[],
pathsWithoutPreviews: <String>[],
@@ -339,16 +351,18 @@
testUsingContext('can detect changes in the pubspec.yaml', () async {
// Create an initial pubspec.
- populatePubspec(previewDetector.projectRoot, 'abc');
+ project = await BasicProjectWithExhaustivePreviews.create(
+ projectRoot: previewDetector.projectRoot,
+ pathsWithPreviews: <String>[],
+ pathsWithoutPreviews: <String>[],
+ );
// Initialize the file watcher.
final PreviewDependencyGraph initialPreviews = await previewDetector.initialize();
expect(initialPreviews, isEmpty);
// Change the contents of the pubspec and verify the callback is invoked.
- await waitForPubspecChangeDetected(
- changeOperation: () => populatePubspec(previewDetector.projectRoot, 'foo'),
- );
+ await waitForPubspecChangeDetected(changeOperation: () => project.touchPubspec());
});
});
}
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_detector/preview_detector_workspace_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_detector/preview_detector_workspace_test.dart
new file mode 100644
index 0000000..108db04
--- /dev/null
+++ b/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_detector/preview_detector_workspace_test.dart
@@ -0,0 +1,197 @@
+// 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:flutter_tools/src/widget_preview/dependency_graph.dart';
+import 'package:flutter_tools/src/widget_preview/preview_detector.dart';
+import 'package:test/test.dart';
+
+import '../../../../src/common.dart';
+import '../../../../src/context.dart';
+import '../utils/preview_detector_test_utils.dart';
+import '../utils/preview_project.dart';
+
+// Note: this test isn't under the general.shard since tests under that directory
+// have a 2000ms time out and these tests write to the real file system and watch
+// directories for changes. This can be slow on heavily loaded machines and cause
+// flaky failures.
+
+void main() {
+ initializeTestPreviewDetectorState();
+ group('$PreviewDetector - Workspace', () {
+ // Note: we don't use a MemoryFileSystem since we don't have a way to
+ // provide it to package:analyzer APIs without writing a significant amount
+ // of wrapper logic.
+ late PreviewDetector previewDetector;
+ late WidgetPreviewWorkspace workspace;
+
+ setUp(() {
+ previewDetector = createTestPreviewDetector();
+ workspace = WidgetPreviewWorkspace(workspaceRoot: previewDetector.projectRoot);
+ });
+
+ tearDown(() async {
+ await previewDetector.dispose();
+ });
+
+ const String simplePreviewSource = '''
+import 'package:flutter/material.dart';
+import 'package:flutter/widget_previews.dart';
+
+@Preview(name: 'preview')
+Widget preview() => Text('Hello world!');
+''';
+
+ const String noPreviewSource = '''
+import 'package:flutter/material.dart';
+
+Widget foo() => Text('Hello world!');
+''';
+
+ testUsingContext(
+ 'can detect previews in existing files in multiple workspace projects',
+ () async {
+ workspace
+ ..createWorkspaceProject(
+ name: 'foo',
+ ).writeFile((path: 'foo.dart', source: simplePreviewSource))
+ ..createWorkspaceProject(
+ name: 'bar',
+ ).writeFile((path: 'bar.dart', source: simplePreviewSource));
+
+ final PreviewDependencyGraph mapping = await previewDetector.initialize();
+ expect(mapping.nodesWithPreviews.length, 2);
+ },
+ );
+
+ testUsingContext('can detect previews in updated files', () async {
+ // Create two projects with existing previews and one without.
+ workspace
+ ..createWorkspaceProject(
+ name: 'foo',
+ ).writeFile((path: 'foo.dart', source: simplePreviewSource))
+ ..createWorkspaceProject(
+ name: 'bar',
+ ).writeFile((path: 'bar.dart', source: simplePreviewSource));
+
+ final WidgetPreviewProject projectBaz = workspace.createWorkspaceProject(name: 'baz')
+ ..writeFile((path: 'baz.dart', source: noPreviewSource));
+
+ // Initialize the file watcher.
+ final PreviewDependencyGraph initialPreviews = await previewDetector.initialize();
+ expect(initialPreviews.nodesWithPreviews.length, 2);
+
+ await waitForChangeDetected(
+ onChangeDetected: (PreviewDependencyGraph updated) {
+ // The new preview in baz.dart should be included in the preview mapping.
+ expect(updated.nodesWithPreviews.length, 3);
+ },
+ changeOperation:
+ () => projectBaz.writeFile((path: 'baz.dart', source: simplePreviewSource)),
+ );
+
+ // Update the file with an existing preview to remove the preview and ensure it triggers
+ // the preview detector.
+ await waitForChangeDetected(
+ onChangeDetected: (PreviewDependencyGraph updated) {
+ // The removed preview in baz.dart should not longer be included in the preview mapping.
+ expect(updated.nodesWithPreviews.length, 2);
+ },
+ changeOperation: () => projectBaz.writeFile((path: 'baz.dart', source: noPreviewSource)),
+ );
+ });
+
+ testUsingContext('can detect previews in newly added projects', () async {
+ // Create two projects with existing previews.
+ workspace
+ ..createWorkspaceProject(
+ name: 'foo',
+ ).writeFile((path: 'foo.dart', source: simplePreviewSource))
+ ..createWorkspaceProject(
+ name: 'bar',
+ ).writeFile((path: 'bar.dart', source: simplePreviewSource));
+
+ // Initialize the file watcher.
+ final PreviewDependencyGraph initialPreviews = await previewDetector.initialize();
+ expect(initialPreviews.nodesWithPreviews.length, 2);
+
+ // Add a new project to the workspace with single preview and verify it's detected.
+ await waitForChangeDetected(
+ onChangeDetected: (PreviewDependencyGraph updated) {
+ // The new preview in baz.dart should be included in the preview mapping.
+ expect(updated.nodesWithPreviews.length, 3);
+ },
+ changeOperation:
+ () =>
+ workspace.createWorkspaceProject(name: 'baz')
+ ..writeFile((path: 'baz.dart', source: simplePreviewSource)),
+ );
+ });
+
+ testUsingContext('can detect previews removed due to deleted project', () async {
+ // Create three projects with existing previews.
+ workspace
+ ..createWorkspaceProject(
+ name: 'foo',
+ ).writeFile((path: 'foo.dart', source: simplePreviewSource))
+ ..createWorkspaceProject(
+ name: 'bar',
+ ).writeFile((path: 'bar.dart', source: simplePreviewSource))
+ ..createWorkspaceProject(
+ name: 'baz',
+ ).writeFile((path: 'baz.dart', source: simplePreviewSource));
+
+ // Initialize the file watcher.
+ final PreviewDependencyGraph initialPreviews = await previewDetector.initialize();
+ expect(initialPreviews.nodesWithPreviews.length, 3);
+
+ await waitForChangeDetected(
+ onChangeDetected: (PreviewDependencyGraph updated) {
+ // The preview in baz.dart in the deleted project should be removed from the preview
+ // mapping.
+ expect(updated.nodesWithPreviews.length, 2);
+ },
+ // Delete the 'baz' project.
+ changeOperation: () => workspace.deleteWorkspaceProject(name: 'baz'),
+ );
+ });
+
+ testUsingContext("can detect changes in a subproject's pubspec.yaml", () async {
+ // Create three empty projects in the same workspace.
+ workspace
+ ..createWorkspaceProject(name: 'foo')
+ ..createWorkspaceProject(name: 'bar');
+ final WidgetPreviewProject bazProject = workspace.createWorkspaceProject(name: 'baz');
+
+ // Initialize the file watcher.
+ final PreviewDependencyGraph initialPreviews = await previewDetector.initialize();
+ expect(initialPreviews, isEmpty);
+
+ // Change the contents of the pubspec and verify the callback is invoked for the right
+ // pubspec.yaml.
+ expect(
+ await waitForPubspecChangeDetected(changeOperation: () => bazProject.touchPubspec()),
+ bazProject.pubspecAbsolutePath,
+ );
+ });
+
+ testUsingContext("can detect changes in a workspace's root pubspec.yaml", () async {
+ // Create three empty projects in the same workspace.
+ workspace
+ ..createWorkspaceProject(name: 'foo')
+ ..createWorkspaceProject(name: 'bar')
+ ..createWorkspaceProject(name: 'baz');
+
+ // Initialize the file watcher.
+ final PreviewDependencyGraph initialPreviews = await previewDetector.initialize();
+ expect(initialPreviews, isEmpty);
+
+ // Change the contents of the pubspec and verify the callback is invoked for the right
+ // pubspec.yaml.
+ expect(
+ await waitForPubspecChangeDetected(changeOperation: () => workspace.touchPubspec()),
+ workspace.pubspecAbsolutePath,
+ );
+ });
+ });
+}
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_detector_graph_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_detector_graph_test.dart
index 4c9024f..2a68034 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_detector_graph_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_detector_graph_test.dart
@@ -31,9 +31,10 @@
late PreviewDetector previewDetector;
late WidgetPreviewProject project;
- setUp(() {
+ setUp(() async {
previewDetector = createTestPreviewDetector();
project = WidgetPreviewProject(projectRoot: previewDetector.projectRoot);
+ await project.initializePubspec();
});
tearDown(() async {
@@ -48,7 +49,7 @@
].forEach(project.writeFile);
final PreviewDependencyGraph graph = await previewDetector.initialize();
expect(graph.keys, containsAll(project.paths));
- expectPreviewDependencyGraphIsWellFormed(graph);
+ expectPreviewDependencyGraphIsWellFormed(project: project, graph: graph);
});
group('library parts', () {
@@ -95,17 +96,18 @@
project.toPreviewPath(lib.path),
}),
);
- expectPreviewDependencyGraphIsWellFormed(initialGraph);
+ expectPreviewDependencyGraphIsWellFormed(project: project, graph: initialGraph);
expect(initialGraph[project.toPreviewPath(lib.path)]!.files, hasLength(3));
});
testUsingContext('with errors in parts', () async {
final PreviewDependencyGraph initialGraph = await previewDetector.initialize();
- expectPreviewDependencyGraphIsWellFormed(initialGraph);
+ expectPreviewDependencyGraphIsWellFormed(project: project, graph: initialGraph);
// Introduce a compilation error into one of the library parts and verify that the library
// and libraries that depend on it have errors.
await expectHasErrors(
+ project: project,
changeOperation:
() => project.writeFile(
withUpdatedSource(libPart1, '${libPart1.source}\ninvalid-symbol;'),
@@ -114,7 +116,10 @@
);
// Fix the compilation error and verify that there's no longer any errors.
- await expectHasNoErrors(changeOperation: () => project.writeFile(libPart1));
+ await expectHasNoErrors(
+ project: project,
+ changeOperation: () => project.writeFile(libPart1),
+ );
});
});
@@ -172,6 +177,7 @@
// Validate the files in dir/ all have transistive errors.
await expectHasErrors(
+ project: project,
changeOperation: () => project.writeFile(toInvalidSource(c)),
filesWithErrors: <WidgetPreviewSourceFile>{a, b, c},
);
@@ -185,7 +191,7 @@
);
// Verify the graph is well formed once the deletion events have been processed.
- expectPreviewDependencyGraphIsWellFormed(initialGraph);
+ expectPreviewDependencyGraphIsWellFormed(project: project, graph: initialGraph);
});
testUsingContext('smoke test', () async {
@@ -201,12 +207,13 @@
// Introduce an error into bar.dart and verify files that have transitive dependencies on
// bar.dart are marked as having errors.
await expectHasErrors(
+ project: project,
changeOperation: () => project.writeFile(toInvalidSource(bar)),
filesWithErrors: project.currentSources,
);
// Remove the error from bar.dart and ensure no files have errors.
- await expectHasNoErrors(changeOperation: () => project.writeFile(bar));
+ await expectHasNoErrors(project: project, changeOperation: () => project.writeFile(bar));
});
testUsingContext('file with error added and removed', () async {
@@ -223,6 +230,7 @@
// the only file with errors.
const WidgetPreviewSourceFile baz = (path: 'baz.dart', source: 'invalid.symbol');
await expectHasErrors(
+ project: project,
changeOperation: () => project.writeFile(baz),
filesWithErrors: <WidgetPreviewSourceFile>{baz},
);
@@ -230,6 +238,7 @@
// Update main.dart to import baz.dart. All files in the project should now have transitive
// errors.
await expectHasErrors(
+ project: project,
changeOperation:
() => project.writeFile((
path: main.path,
@@ -240,12 +249,13 @@
// Delete baz.dart. main.dart should continue to have an error.
await expectHasErrors(
+ project: project,
changeOperation: () => project.removeFile(baz),
filesWithErrors: <WidgetPreviewSourceFile>{main},
);
// Restore main.dart to remove the baz.dart import and clear the errors.
- await expectHasNoErrors(changeOperation: () => project.writeFile(main));
+ await expectHasNoErrors(project: project, changeOperation: () => project.writeFile(main));
});
testUsingContext(
@@ -263,12 +273,13 @@
// Add baz.dart, which contains errors. Since no other files import baz.dart, it should be
// the only file with errors.
await expectHasErrors(
+ project: project,
changeOperation: () => project.writeFile(toInvalidSource(foo)),
filesWithErrors: <WidgetPreviewSourceFile>{foo, main},
);
// Delete baz.dart. main.dart should continue to have an error.
- await expectHasNoErrors(changeOperation: () => project.writeFile(foo));
+ await expectHasNoErrors(project: project, changeOperation: () => project.writeFile(foo));
},
);
});
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/utils/preview_details_matcher.dart b/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/utils/preview_details_matcher.dart
index 55ab8cf..13bc186 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/utils/preview_details_matcher.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/utils/preview_details_matcher.dart
@@ -12,6 +12,7 @@
/// A [Matcher] that verifies each property of a `@Preview` declaration matches an expected value.
class PreviewDetailsMatcher extends Matcher {
PreviewDetailsMatcher({
+ required this.packageName,
required this.functionName,
required this.isBuilder,
this.name,
@@ -30,6 +31,7 @@
final String functionName;
final bool isBuilder;
+ final String packageName;
// Proivde when the expected expression for 'name' is a literal.
final String? name;
@@ -88,6 +90,7 @@
}
}
+ checkPropertyMatch(name: 'packageName', actual: item.packageName, expected: packageName);
checkPropertyMatch(name: 'functionName', actual: item.functionName, expected: functionName);
checkPropertyMatch(name: 'isBuilder', actual: item.isBuilder, expected: isBuilder);
checkPropertyMatch(
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/utils/preview_detector_test_utils.dart b/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/utils/preview_detector_test_utils.dart
index ad8b39d..3c2a902 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/utils/preview_detector_test_utils.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/utils/preview_detector_test_utils.dart
@@ -19,7 +19,7 @@
// Global state that must be cleaned up by `tearDown` in initializeTestPreviewDetectorState.
void Function(PreviewDependencyGraph)? _onChangeDetectedImpl;
-void Function()? _onPubspecChangeDetected;
+void Function(String path)? _onPubspecChangeDetected;
Directory? _projectRoot;
late FileSystem _fs;
@@ -59,19 +59,21 @@
_onChangeDetectedImpl!(mapping);
}
-void _onPubspecChangeDetectedRoot() {
- _onPubspecChangeDetected!();
+void _onPubspecChangeDetectedRoot(String path) {
+ _onPubspecChangeDetected?.call(path);
}
/// Test the files included in [filesWithErrors] contain errors after executing [changeOperation].
Future<void> expectHasErrors({
+ required WidgetPreviewProject project,
required void Function() changeOperation,
required Set<WidgetPreviewSourceFile> filesWithErrors,
}) async {
await waitForChangeDetected(
onChangeDetected:
(PreviewDependencyGraph updated) => expectPreviewDependencyGraphIsWellFormed(
- updated,
+ project: project,
+ graph: updated,
expectedFilesWithErrors: filesWithErrors,
),
changeOperation: changeOperation,
@@ -80,24 +82,28 @@
/// Test dependency graph generated as a result of [changeOperation] contains no compile time
/// errors.
-Future<void> expectHasNoErrors({required void Function() changeOperation}) async {
+Future<void> expectHasNoErrors({
+ required WidgetPreviewProject project,
+ required void Function() changeOperation,
+}) async {
await expectHasErrors(
+ project: project,
changeOperation: changeOperation,
filesWithErrors: const <WidgetPreviewSourceFile>{},
);
}
/// Waits for a pubspec changed event to be detected after executing [changeOperation].
-Future<void> waitForPubspecChangeDetected({required void Function() changeOperation}) async {
- final Completer<void> completer = Completer<void>();
- _onPubspecChangeDetected = () {
+Future<String> waitForPubspecChangeDetected({required void Function() changeOperation}) {
+ final Completer<String> completer = Completer<String>();
+ _onPubspecChangeDetected = (String path) {
if (completer.isCompleted) {
return;
}
- completer.complete();
+ completer.complete(path);
};
changeOperation();
- await completer.future;
+ return completer.future;
}
/// Waits for a change detected event after executing [changeOperation].
@@ -139,10 +145,22 @@
await completer.future;
}
+extension PreviewDependencyGraphExtensions on PreviewDependencyGraph {
+ /// Returns a subset of dependency graph consisting only of library nodes containing previews.
+ PreviewDependencyGraph get nodesWithPreviews {
+ return PreviewDependencyGraph.fromEntries(
+ entries.where(
+ (MapEntry<PreviewPath, LibraryPreviewNode> element) => element.value.previews.isNotEmpty,
+ ),
+ );
+ }
+}
+
/// Walks the [graph] to verify its structure and that all files contained in
/// [expectedFilesWithErrors] actually contain errors.
-void expectPreviewDependencyGraphIsWellFormed(
- PreviewDependencyGraph graph, {
+void expectPreviewDependencyGraphIsWellFormed({
+ required WidgetPreviewProject project,
+ required PreviewDependencyGraph graph,
Set<WidgetPreviewSourceFile> expectedFilesWithErrors = const <WidgetPreviewSourceFile>{},
}) {
final Set<LibraryPreviewNode> nodesWithErrors = <LibraryPreviewNode>{};
@@ -177,10 +195,7 @@
expect(
filesWithTransitiveErrors,
expectedFilesWithErrors
- .map(
- (WidgetPreviewSourceFile file) =>
- previewPathForFile(projectRoot: _projectRoot!, path: file.path),
- )
+ .map((WidgetPreviewSourceFile file) => project.toPreviewPath(file.path))
.toSet(),
);
}
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/utils/preview_project.dart b/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/utils/preview_project.dart
index 842839a..a529413 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/utils/preview_project.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/utils/preview_project.dart
@@ -8,26 +8,99 @@
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/widget_preview/dependency_graph.dart';
import 'package:meta/meta.dart';
+import 'package:package_config/package_config.dart';
import '../../../../src/common.dart';
typedef WidgetPreviewSourceFile = ({String path, String source});
-PreviewPath previewPathForFile({required Directory projectRoot, required String path}) {
- final File file = projectRoot.childDirectory('lib').childFile(path);
- return (path: file.path, uri: file.uri);
+const String _kPubspec = 'pubspec.yaml';
+
+class WidgetPreviewWorkspace {
+ WidgetPreviewWorkspace({required this.workspaceRoot})
+ : _packagesRoot = workspaceRoot.childDirectory('packages')..createSync(recursive: true),
+ _pubspecYaml = workspaceRoot.childFile(_kPubspec)..createSync();
+
+ final Directory workspaceRoot;
+ final Directory _packagesRoot;
+ final File _pubspecYaml;
+
+ final Map<String, WidgetPreviewProject> _packages = <String, WidgetPreviewProject>{};
+
+ /// The absolute path to the workspace's pubspec.yaml.
+ String get pubspecAbsolutePath => _pubspecYaml.absolute.path;
+
+ /// "Modifies" the workspace's pubspec.yaml.
+ void touchPubspec() {
+ _pubspecYaml.setLastModifiedSync(DateTime.now());
+ }
+
+ WidgetPreviewProject createWorkspaceProject({required String name}) {
+ if (_packages.containsKey(name)) {
+ throw StateError('Project with name "$name" already exists.');
+ }
+ final WidgetPreviewProject project = WidgetPreviewProject(
+ projectRoot: _packagesRoot.childDirectory(name)..createSync(),
+ inWorkspace: true,
+ );
+ project._writePubspec(project.pubspecContents);
+ _packages[name] = project;
+ _updatePubspec();
+ return project;
+ }
+
+ void deleteWorkspaceProject({required String name}) {
+ if (!_packages.containsKey(name)) {
+ throw StateError('Project with name "$name" does not exist.');
+ }
+ _packages[name]!.projectRoot.deleteSync(recursive: true);
+ _updatePubspec();
+ }
+
+ void _updatePubspec() {
+ final StringBuffer pubspec = StringBuffer('workspace:\n');
+ for (final String package in _packages.keys) {
+ pubspec.writeln(' - packages/$package');
+ }
+ _pubspecYaml.writeAsStringSync(pubspec.toString());
+ }
}
/// A utility class used to manage a fake Flutter project for widget preview testing.
class WidgetPreviewProject {
- WidgetPreviewProject({required this.projectRoot})
- : _libDirectory = projectRoot.childDirectory('lib')..createSync(recursive: true);
+ WidgetPreviewProject({required this.projectRoot, this.inWorkspace = false})
+ : _libDirectory = projectRoot.childDirectory('lib')..createSync(recursive: true),
+ _pubspecYaml = projectRoot.childFile(_kPubspec)..createSync();
+
+ /// The name for the package defined by this project.
+ String get packageName => 'foo';
+
+ /// The initial contents of the pubspec.yaml for the project.
+ String get pubspecContents => '''
+name: $packageName
+
+${inWorkspace ? 'resolution: workspace' : ''}
+
+environment:
+ sdk: ^3.7.0
+
+dependencies:
+ flutter:
+ sdk: flutter
+''';
/// The root of the fake project.
///
/// This should always be set to [PreviewDetector.projectRoot].
late final Directory projectRoot;
late final Directory _libDirectory;
+ final File _pubspecYaml;
+
+ /// The absolute path to the project's pubspec.yaml.
+ String get pubspecAbsolutePath => _pubspecYaml.absolute.path;
+
+ /// Set to true if the project is part of a workspace.
+ final bool inWorkspace;
Set<WidgetPreviewSourceFile> get currentSources => _currentSources.values.toSet();
final Map<String, WidgetPreviewSourceFile> _currentSources = <String, WidgetPreviewSourceFile>{};
@@ -36,8 +109,36 @@
/// Builds a [PreviewPath] based on [path] using the [projectRoot]'s `lib/` directory as the
/// path root.
- PreviewPath toPreviewPath(String path) =>
- previewPathForFile(projectRoot: projectRoot, path: path);
+ PreviewPath toPreviewPath(String path) {
+ final File file = _libDirectory.childFile(path);
+
+ return (
+ path: file.path,
+ uri: PackageConfig(<Package>[Package(packageName, projectRoot.uri)]).toPackageUri(file.uri)!,
+ );
+ }
+
+ /// Writes `pubspec.yaml` and `.dart_tool/package_config.json` at [projectRoot].
+ Future<void> initializePubspec() async {
+ _writePubspec(pubspecContents);
+
+ await savePackageConfig(
+ PackageConfig(<Package>[Package(packageName, projectRoot.uri)]),
+ projectRoot,
+ );
+ }
+
+ /// "Modifies" the project's pubspec.yaml.
+ void touchPubspec() {
+ _pubspecYaml.setLastModifiedSync(DateTime.now());
+ }
+
+ /// Updates the content of the project's pubspec.yaml.
+ void _writePubspec(String contents) {
+ projectRoot.childFile(_kPubspec)
+ ..createSync(recursive: true)
+ ..writeAsStringSync(contents);
+ }
/// Writes the contents of [file] to the file system.
@mustCallSuper
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/widget_preview_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/widget_preview_test.dart
index c2ca210..26fc421 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/widget_preview_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/widget_preview_test.dart
@@ -49,62 +49,14 @@
});
testUsingContext(
- 'can create a pubspec.yaml for the preview scaffold including root project assets',
+ 'can create a pubspec.yaml for the preview scaffold including root project deferred components',
() {
final FlutterManifest root = rootProject.manifest;
- final FlutterManifest emptyPreviewManifest =
- rootProject.widgetPreviewScaffoldProject.manifest;
final FlutterManifest updated = pubspecBuilder.buildPubspec(
- rootManifest: rootProject.manifest,
+ rootProject: rootProject,
widgetPreviewManifest: rootProject.widgetPreviewScaffoldProject.manifest,
);
- final List<AssetsEntry> rootAssets = root.assets;
- final List<AssetsEntry> updatedAssets = updated.assets;
- expect(updatedAssets.length, rootAssets.length);
- for (int i = 0; i < rootAssets.length; ++i) {
- final AssetsEntry rootEntry = rootAssets[i];
- final AssetsEntry updatedEntry = updatedAssets[i];
- expect(updatedEntry, PreviewPubspecBuilder.transformAssetsEntry(rootEntry));
- }
-
- final int emptyPreviewFontCount = emptyPreviewManifest.fonts.length;
- final int expectedFontCount = root.fonts.length + emptyPreviewFontCount;
- expect(updated.fonts.length, expectedFontCount);
-
- // Verify that the updated preview scaffold pubspec includes fonts needed by
- // the previewer.
- for (int i = 0; i < emptyPreviewFontCount; ++i) {
- final Font defaultPreviewerFont = emptyPreviewManifest.fonts[i];
- final Font updatedFont = updated.fonts[i];
- expect(updatedFont.familyName, defaultPreviewerFont.familyName);
- expect(updatedFont.fontAssets.length, defaultPreviewerFont.fontAssets.length);
- for (int j = 0; j < defaultPreviewerFont.fontAssets.length; ++j) {
- final FontAsset rootFontAsset = defaultPreviewerFont.fontAssets[j];
- final FontAsset updatedFontAsset = updatedFont.fontAssets[j];
- expect(updatedFontAsset.descriptor, rootFontAsset.descriptor);
- }
- }
-
- // Verify fonts from the root project are included in the updated preview
- // scaffold pubspec.
- for (int i = emptyPreviewFontCount; i < expectedFontCount; ++i) {
- final Font rootFont = root.fonts[i - emptyPreviewFontCount];
- final Font updatedFont = updated.fonts[i];
- expect(updatedFont.familyName, rootFont.familyName);
- expect(updatedFont.fontAssets.length, rootFont.fontAssets.length);
- for (int j = 0; j < rootFont.fontAssets.length; ++j) {
- final FontAsset rootFontAsset = rootFont.fontAssets[j];
- final FontAsset updatedFontAsset = updatedFont.fontAssets[j];
- expect(
- updatedFontAsset.descriptor,
- PreviewPubspecBuilder.transformFontAsset(rootFontAsset).descriptor,
- );
- }
- }
-
- expect(updated.shaders, root.shaders.map(PreviewPubspecBuilder.transformAssetUri));
-
expect(updated.deferredComponents?.length, root.deferredComponents?.length);
if (root.deferredComponents != null) {
for (int i = 0; i < root.deferredComponents!.length; ++i) {
@@ -200,4 +152,7 @@
mainLibName: 'my_app',
);
}();
+
+ @override
+ final List<FlutterProject> workspaceProjects = <FlutterProject>[];
}
diff --git a/packages/flutter_tools/test/commands.shard/permeable/widget_preview_test.dart b/packages/flutter_tools/test/commands.shard/permeable/widget_preview_test.dart
index 208fb57..9a2d246 100644
--- a/packages/flutter_tools/test/commands.shard/permeable/widget_preview_test.dart
+++ b/packages/flutter_tools/test/commands.shard/permeable/widget_preview_test.dart
@@ -217,6 +217,7 @@
List<_i1.WidgetPreview> previews() => [
_i1.WidgetPreview(
+ packageName: 'flutter_project',
name: 'preview',
builder: () => _i2.preview(),
)
diff --git a/packages/flutter_tools/test/general.shard/widget_preview/preview_manifest_test.dart b/packages/flutter_tools/test/general.shard/widget_preview/preview_manifest_test.dart
index e2fd451..79976db 100644
--- a/packages/flutter_tools/test/general.shard/widget_preview/preview_manifest_test.dart
+++ b/packages/flutter_tools/test/general.shard/widget_preview/preview_manifest_test.dart
@@ -17,6 +17,7 @@
import '../../src/common.dart';
import '../../src/context.dart';
+// TODO(bkonyi): test pubspec change detection for workspaces
void main() {
group('$PreviewManifest', () {
late FlutterProject rootProject;
@@ -55,7 +56,7 @@
)
as PreviewManifestContents;
- expect(manifest.containsKey(PreviewManifest.kPubspecHash), true);
+ expect(manifest.containsKey(PreviewManifest.kPubspecHashes), true);
expect(manifest.containsKey(PreviewManifest.kManifestVersion), true);
expect(manifest.containsKey(PreviewManifest.kSdkVersion), true);
});
diff --git a/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/lib/src/widget_preview.dart b/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/lib/src/widget_preview.dart
index b063e62..bd55c12 100644
--- a/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/lib/src/widget_preview.dart
+++ b/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/lib/src/widget_preview.dart
@@ -21,6 +21,7 @@
/// properties.
const WidgetPreview({
required this.builder,
+ this.packageName,
this.name,
this.size,
this.textScaleFactor,
@@ -29,6 +30,12 @@
this.localizations,
});
+ /// The name of the package in which a preview was defined.
+ ///
+ /// For example, if a preview is defined in 'package:foo/src/bar.dart', this
+ /// will have the value 'foo'.
+ final String? packageName;
+
/// A description to be displayed alongside the preview.
///
/// If not provided, no name will be associated with the preview.
diff --git a/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/lib/src/widget_preview_rendering.dart b/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/lib/src/widget_preview_rendering.dart
index 42018bf..1bb327e 100644
--- a/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/lib/src/widget_preview_rendering.dart
+++ b/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/lib/src/widget_preview_rendering.dart
@@ -330,6 +330,21 @@
child: preview,
);
+ // Override the asset resolution behavior to automatically insert
+ // 'packages/$packageName/` in front of non-package paths as some previews
+ // may reference assets that are within the current project and wouldn't
+ // normally require a package specifier.
+ // TODO(bkonyi): this doesn't modify the behavior of asset loading logic in
+ // the engine implementation. This means that any asset loading done by
+ // APIs provided in dart:ui won't work correctly for non-package asset
+ // paths (e.g., shaders loaded by `FragmentProgram.fromAsset()`).
+ //
+ // See https://github.com/flutter/flutter/issues/171284
+ preview = DefaultAssetBundle(
+ bundle: PreviewAssetBundle(packageName: widget.preview.packageName),
+ child: preview,
+ );
+
preview = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -683,12 +698,28 @@
}
/// Custom [AssetBundle] used to map original asset paths from the parent
-/// project to those in the preview project.
+/// projects to those in the preview project.
class PreviewAssetBundle extends PlatformAssetBundle {
+ PreviewAssetBundle({required this.packageName});
+
+ /// The name of the package in which a preview was defined.
+ ///
+ /// For example, if a preview is defined in 'package:foo/src/bar.dart', this
+ /// will have the value 'foo'.
+ ///
+ /// This should only be null if the preview is defined in a file that's not
+ /// part of a Flutter library (e.g., is defined in a test).
+ // TODO(bkonyi): verify what the behavior should be in this scenario.
+ final String? packageName;
+
// Assets shipped via package dependencies have paths that start with
// 'packages'.
static const String _kPackagesPrefix = 'packages';
+ // TODO(bkonyi): when loading an invalid asset path that doesn't start with
+ // 'packages', this throws a FlutterError referencing the modified key
+ // instead of the original. We should catch the error and rethrow one with
+ // the original key in the error message.
@override
Future<ByteData> load(String key) {
// These assets are always present or are shipped via a package and aren't
@@ -697,12 +728,13 @@
if (key == 'AssetManifest.bin' ||
key == 'AssetManifest.json' ||
key == 'FontManifest.json' ||
- key.startsWith(_kPackagesPrefix)) {
+ key.startsWith(_kPackagesPrefix) ||
+ packageName == null) {
return super.load(key);
}
- // Other assets are from the parent project. Map their keys to those found
- // in the pubspec.yaml of the preview envirnment.
- return super.load('../../$key');
+ // Other assets are from the parent project. Map their keys to package
+ // paths corresponding to the package containing the preview.
+ return super.load(_toPackagePath(key));
}
@override
@@ -712,9 +744,11 @@
return ImmutableBuffer.fromUint8List(Uint8List.sublistView(bytes));
}
return await ImmutableBuffer.fromAsset(
- key.startsWith(_kPackagesPrefix) ? key : '../../$key',
+ key.startsWith(_kPackagesPrefix) ? key : _toPackagePath(key),
);
}
+
+ String _toPackagePath(String key) => '$_kPackagesPrefix/$packageName/$key';
}
/// Main entrypoint for the widget previewer.
@@ -875,16 +909,13 @@
debugShowCheckedModeBanner: false,
home: Material(
color: Colors.transparent,
- child: DefaultAssetBundle(
- bundle: PreviewAssetBundle(),
- child: Stack(
- children: [
- // Display the previewer
- _displayPreviewer(previewView),
- // Display the layout toggle buttons
- _displayToggleLayoutButtons(),
- ],
- ),
+ child: Stack(
+ children: [
+ // Display the previewer
+ _displayPreviewer(previewView),
+ // Display the layout toggle buttons
+ _displayToggleLayoutButtons(),
+ ],
),
),
);