Skip to content

Commit 4b14308

Browse files
authored
Add support for injecting assets into the widget_preview_scaffold (#159859)
The generated widget_preview_scaffold project needs to explicitly reference the assets from the parent project's pubspec.yaml. This change updates flutter widget-preview start to read the parent project's pubspec.yaml and add references to the assets listed to the widget_preview_scaffold's pubspec.yaml. If generate: true is set in the parent project, a reference to the autogenerated flutter_gen package is manually added to the widget_preview_scaffold's package_config.json.
1 parent 2eee054 commit 4b14308

File tree

7 files changed

+689
-14
lines changed

7 files changed

+689
-14
lines changed

packages/flutter_tools/lib/src/base/deferred_component.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,20 @@ class DeferredComponent {
7878
};
7979
}
8080

81+
/// Returns a descriptor of the component to be used when modifying a
82+
/// pubspec.yaml.
83+
Map<String, Object?> get descriptor {
84+
return <String, Object?>{
85+
'name': name,
86+
if (libraries.isNotEmpty)
87+
'libraries': libraries.toList(),
88+
if (assets.isNotEmpty)
89+
'assets': assets.map(
90+
(AssetsEntry e) => e.descriptor,
91+
).toList(),
92+
};
93+
}
94+
8195
/// Provides a human readable string representation of the
8296
/// configuration.
8397
@override

packages/flutter_tools/lib/src/commands/widget_preview.dart

Lines changed: 183 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,17 @@
33
// found in the LICENSE file.
44

55
import 'package:args/args.dart';
6+
import 'package:meta/meta.dart';
67

78
import '../base/common.dart';
9+
import '../base/deferred_component.dart';
810
import '../base/file_system.dart';
911
import '../base/platform.dart';
1012
import '../cache.dart';
13+
import '../convert.dart';
1114
import '../dart/pub.dart';
15+
import '../flutter_manifest.dart';
16+
1217
import '../globals.dart' as globals;
1318
import '../project.dart';
1419
import '../runner/flutter_command.dart';
@@ -86,16 +91,17 @@ class WidgetPreviewStartCommand extends FlutterCommand
8691
@override
8792
Future<FlutterCommandResult> runCommand() async {
8893
final FlutterProject rootProject = getRootProject();
94+
final Directory widgetPreviewScaffold = rootProject.widgetPreviewScaffold;
8995

9096
// Check to see if a preview scaffold has already been generated. If not,
9197
// generate one.
92-
if (!rootProject.widgetPreviewScaffold.existsSync()) {
98+
if (!widgetPreviewScaffold.existsSync()) {
9399
globals.logger.printStatus(
94-
'Creating widget preview scaffolding at: ${rootProject.widgetPreviewScaffold.path}',
100+
'Creating widget preview scaffolding at: ${widgetPreviewScaffold.path}',
95101
);
96102
await generateApp(
97103
<String>['widget_preview_scaffold'],
98-
rootProject.widgetPreviewScaffold,
104+
widgetPreviewScaffold,
99105
createTemplateContext(
100106
organization: 'flutter',
101107
projectName: 'widget_preview_scaffold',
@@ -109,17 +115,183 @@ class WidgetPreviewStartCommand extends FlutterCommand
109115
overwrite: true,
110116
generateMetadata: false,
111117
);
118+
await _populatePreviewPubspec(rootProject: rootProject);
119+
}
120+
return FlutterCommandResult.success();
121+
}
122+
123+
@visibleForTesting
124+
static const Map<String, String> flutterGenPackageConfigEntry =
125+
<String, String>{
126+
'name': 'flutter_gen',
127+
'rootUri': '../../flutter_gen',
128+
'languageVersion': '2.12',
129+
};
112130

113-
if (shouldCallPubGet) {
114-
await pub.get(
115-
context: PubContext.create,
116-
project: rootProject.widgetPreviewScaffoldProject,
117-
offline: offline,
118-
outputMode: PubOutputMode.summaryOnly,
131+
/// Maps asset URIs to relative paths for the widget preview project to
132+
/// include.
133+
@visibleForTesting
134+
static Uri transformAssetUri(Uri uri) {
135+
// Assets provided by packages always start with 'packages' and do not
136+
// require their URIs to be updated.
137+
if (uri.path.startsWith('packages')) {
138+
return uri;
139+
}
140+
// Otherwise, the asset is contained within the root project and needs
141+
// to be referenced from the widget preview scaffold project's pubspec.
142+
return Uri(path: '../../${uri.path}');
143+
}
144+
145+
@visibleForTesting
146+
static AssetsEntry transformAssetsEntry(AssetsEntry asset) {
147+
return AssetsEntry(
148+
uri: transformAssetUri(asset.uri),
149+
flavors: asset.flavors,
150+
transformers: asset.transformers,
151+
);
152+
}
153+
154+
@visibleForTesting
155+
static FontAsset transformFontAsset(FontAsset asset) {
156+
return FontAsset(
157+
transformAssetUri(asset.assetUri),
158+
weight: asset.weight,
159+
style: asset.style,
160+
);
161+
}
162+
163+
@visibleForTesting
164+
static DeferredComponent transformDeferredComponent(
165+
DeferredComponent component) {
166+
return DeferredComponent(
167+
name: component.name,
168+
// TODO(bkonyi): verify these library paths are always package: paths from the parent project.
169+
libraries: component.libraries,
170+
assets: component.assets.map(transformAssetsEntry).toList(),
171+
);
172+
}
173+
174+
@visibleForTesting
175+
FlutterManifest buildPubspec({
176+
required FlutterManifest rootManifest,
177+
required FlutterManifest widgetPreviewManifest,
178+
}) {
179+
final List<AssetsEntry> assets =
180+
rootManifest.assets.map(transformAssetsEntry).toList();
181+
182+
final List<Font> fonts = rootManifest.fonts.map(
183+
(Font font) {
184+
return Font(
185+
font.familyName,
186+
font.fontAssets.map(transformFontAsset).toList(),
119187
);
120-
}
188+
},
189+
).toList();
190+
191+
final List<Uri> shaders =
192+
rootManifest.shaders.map(transformAssetUri).toList();
193+
194+
final List<Uri> models =
195+
rootManifest.models.map(transformAssetUri).toList();
196+
197+
final List<DeferredComponent>? deferredComponents = rootManifest
198+
.deferredComponents
199+
?.map(transformDeferredComponent)
200+
.toList();
201+
202+
return widgetPreviewManifest.copyWith(
203+
logger: globals.logger,
204+
assets: assets,
205+
fonts: fonts,
206+
shaders: shaders,
207+
models: models,
208+
deferredComponents: deferredComponents,
209+
);
210+
}
211+
212+
Future<void> _populatePreviewPubspec({
213+
required FlutterProject rootProject,
214+
}) async {
215+
final FlutterProject widgetPreviewScaffoldProject =
216+
rootProject.widgetPreviewScaffoldProject;
217+
218+
// Overwrite the pubspec for the preview scaffold project to include assets
219+
// from the root project.
220+
widgetPreviewScaffoldProject.replacePubspec(
221+
buildPubspec(
222+
rootManifest: rootProject.manifest,
223+
widgetPreviewManifest: widgetPreviewScaffoldProject.manifest,
224+
),
225+
);
226+
227+
// Adds a path dependency on the parent project so previews can be
228+
// imported directly into the preview scaffold.
229+
const String pubAdd = 'add';
230+
await pub.interactively(
231+
<String>[
232+
pubAdd,
233+
'--directory',
234+
widgetPreviewScaffoldProject.directory.path,
235+
'${rootProject.manifest.appName}:{"path":${rootProject.directory.path}}',
236+
],
237+
context: PubContext.pubAdd,
238+
command: pubAdd,
239+
touchesPackageConfig: true,
240+
);
241+
242+
// Generate package_config.json.
243+
await pub.get(
244+
context: PubContext.create,
245+
project: widgetPreviewScaffoldProject,
246+
offline: offline,
247+
outputMode: PubOutputMode.summaryOnly,
248+
);
249+
250+
if (rootProject.manifest.generateSyntheticPackage) {
251+
maybeAddFlutterGenToPackageConfig(
252+
rootProject: rootProject,
253+
);
121254
}
122-
return FlutterCommandResult.success();
255+
}
256+
257+
/// Manually adds an entry for package:flutter_gen to the preview scaffold's
258+
/// package_config.json if the target project makes use of localization.
259+
///
260+
/// The Flutter Tool does this when running a Flutter project with
261+
/// localization instead of modifying the user's pubspec.yaml to depend on it
262+
/// as a path dependency. Unfortunately, the preview scaffold still needs to
263+
/// add it directly to its package_config.json as the generated package name
264+
/// isn't actually flutter_gen, which pub doesn't really like, and using the
265+
/// actual package name will break applications which import
266+
/// package:flutter_gen.
267+
void maybeAddFlutterGenToPackageConfig({
268+
required FlutterProject rootProject,
269+
}) {
270+
if (!rootProject.manifest.generateSyntheticPackage) {
271+
return;
272+
}
273+
final FlutterProject widgetPreviewScaffoldProject =
274+
rootProject.widgetPreviewScaffoldProject;
275+
final File packageConfig = widgetPreviewScaffoldProject.packageConfig;
276+
final String previewPackageConfigPath = packageConfig.path;
277+
if (!packageConfig.existsSync()) {
278+
throw StateError(
279+
"Could not find preview project's package_config.json at "
280+
'$previewPackageConfigPath',
281+
);
282+
}
283+
final Map<String, Object?> packageConfigJson = json.decode(
284+
packageConfig.readAsStringSync(),
285+
) as Map<String, Object?>;
286+
(packageConfigJson['packages'] as List<dynamic>?)!
287+
.cast<Map<String, String>>()
288+
.add(flutterGenPackageConfigEntry);
289+
packageConfig.writeAsStringSync(
290+
json.encode(packageConfigJson),
291+
);
292+
globals.logger.printStatus(
293+
'Added flutter_gen dependency to $previewPackageConfigPath',
294+
);
123295
}
124296
}
125297

packages/flutter_tools/lib/src/flutter_manifest.dart

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,71 @@ class FlutterManifest {
6464
return pubspec;
6565
}
6666

67+
/// Creates a copy of the current manifest with some subset of properties
68+
/// modified.
69+
FlutterManifest copyWith({
70+
required Logger logger,
71+
List<AssetsEntry>? assets,
72+
List<Font>? fonts,
73+
List<Uri>? shaders,
74+
List<Uri>? models,
75+
List<DeferredComponent>? deferredComponents,
76+
}) {
77+
final FlutterManifest copy = FlutterManifest._(logger: _logger);
78+
copy._descriptor = <String, Object?>{..._descriptor};
79+
copy._flutterDescriptor = <String, Object?>{..._flutterDescriptor};
80+
81+
if (assets != null && assets.isNotEmpty) {
82+
copy._flutterDescriptor['assets'] = YamlList.wrap(
83+
<Object?>[
84+
for (final AssetsEntry asset in assets)
85+
asset.descriptor,
86+
],
87+
);
88+
}
89+
90+
if (fonts != null && fonts.isNotEmpty) {
91+
copy._flutterDescriptor['fonts'] = YamlList.wrap(
92+
<Map<String, Object?>>[
93+
for (final Font font in fonts)
94+
font.descriptor,
95+
],
96+
);
97+
}
98+
99+
if (shaders != null && shaders.isNotEmpty) {
100+
copy._flutterDescriptor['shaders'] = YamlList.wrap(
101+
shaders.map(
102+
(Uri uri) => uri.toString(),
103+
).toList(),
104+
);
105+
}
106+
107+
if (models != null && models.isNotEmpty) {
108+
copy._flutterDescriptor['models'] = YamlList.wrap(
109+
models.map(
110+
(Uri uri) => uri.toString(),
111+
).toList(),
112+
);
113+
}
114+
115+
if (deferredComponents != null && deferredComponents.isNotEmpty) {
116+
copy._flutterDescriptor['deferred-components'] = YamlList.wrap(
117+
deferredComponents.map(
118+
(DeferredComponent dc) => dc.descriptor,
119+
).toList()
120+
);
121+
}
122+
123+
copy._descriptor['flutter'] = YamlMap.wrap(copy._flutterDescriptor);
124+
125+
if (!_validate(YamlMap.wrap(copy._descriptor), logger)) {
126+
throw StateError('Generated invalid pubspec.yaml.');
127+
}
128+
129+
return copy;
130+
}
131+
67132
final Logger _logger;
68133

69134
/// A map representation of the entire `pubspec.yaml` file.
@@ -380,6 +445,10 @@ class FlutterManifest {
380445
}
381446

382447
String? get defaultFlavor => _flutterDescriptor['default-flavor'] as String?;
448+
449+
YamlMap toYaml() {
450+
return YamlMap.wrap(_descriptor);
451+
}
383452
}
384453

385454
class Font {
@@ -712,6 +781,21 @@ class AssetsEntry {
712781
final Set<String> flavors;
713782
final List<AssetTransformerEntry> transformers;
714783

784+
Object? get descriptor {
785+
if (transformers.isEmpty && flavors.isEmpty) {
786+
return uri.toString();
787+
}
788+
return <String, Object?> {
789+
_pathKey: uri.toString(),
790+
if (flavors.isNotEmpty)
791+
_flavorKey: flavors.toList(),
792+
if (transformers.isNotEmpty)
793+
_transformersKey: transformers.map(
794+
(AssetTransformerEntry e) => e.descriptor,
795+
).toList(),
796+
};
797+
}
798+
715799
static const String _pathKey = 'path';
716800
static const String _flavorKey = 'flavors';
717801
static const String _transformersKey = 'transformers';
@@ -858,6 +942,17 @@ final class AssetTransformerEntry {
858942
final String package;
859943
final List<String>? args;
860944

945+
Map<String, Object?> get descriptor {
946+
return <String, Object?>{
947+
_kPackage: package,
948+
if (args != null)
949+
_kArgs: args,
950+
};
951+
}
952+
953+
static const String _kPackage = 'package';
954+
static const String _kArgs = 'args';
955+
861956
static (AssetTransformerEntry? entry, List<String> errors) tryParse(Object? yaml) {
862957
if (yaml == null) {
863958
return (null, <String>['Transformer entry is null.']);

0 commit comments

Comments
 (0)