Commit 02c10b78 authored by Sarah Zakarias's avatar Sarah Zakarias Committed by GitHub

Bundle assets used in packages (#11751)

parent baf3b45e
......@@ -589,27 +589,94 @@ class MemoryImage extends ImageProvider<MemoryImage> {
/// Fetches an image from an [AssetBundle], associating it with the given scale.
///
/// This implementation requires an explicit final [name] and [scale] on
/// This implementation requires an explicit final [assetName] and [scale] on
/// construction, and ignores the device pixel ratio and size in the
/// configuration passed into [resolve]. For a resolution-aware variant that
/// uses the configuration to pick an appropriate image based on the device
/// pixel ratio and size, see [AssetImage].
///
/// ## Fetching assets
///
/// When fetching an image provided by the app itself, use the [assetName]
/// argument to name the asset to choose. For instance, consider a directory
/// `icons` with an image `heart.png`. First, the [pubspec.yaml] of the project
/// should specify its assets in the `flutter` section:
///
/// ```yaml
/// flutter:
/// assets:
/// - icons/heart.png
/// ```
///
/// Then, to fetch the image and associate it with scale `1.5`, use
///
/// ```dart
/// new AssetImage('icons/heart.png', scale: 1.5)
/// ```
///
///## Assets in packages
///
/// To fetch an asset from a package, the [package] argument must be provided.
/// For instance, suppose the structure above is inside a package called
/// `my_icons`. Then to fetch the image, use:
///
/// ```dart
/// new AssetImage('icons/heart.png', scale: 1.5, package: 'my_icons')
/// ```
///
/// Assets used by the package itself should also be fetched using the [package]
/// argument as above.
///
/// If the desired asset is specified in the [pubspec.yaml] of the package, it
/// is bundled automatically with the app. In particular, assets used by the
/// package itself must be specified in its [pubspec.yaml].
///
/// A package can also choose to have assets in its 'lib/' folder that are not
/// specified in its [pubspec.yaml]. In this case for those images to be
/// bundled, the app has to specify which ones to include. For instance a
/// package named `fancy_backgrounds` could have:
///
/// ```
/// lib/backgrounds/background1.png
/// lib/backgrounds/background2.png
/// lib/backgrounds/background3.png
///```
///
/// To include, say the first image, the [pubspec.yaml] of the app should specify
/// it in the `assets` section:
///
/// ```yaml
/// assets:
/// - packages/fancy_backgrounds/backgrounds/background1.png
/// ```
///
/// Note that the `lib/` is implied, so it should not be included in the asset
/// path.
///
class ExactAssetImage extends AssetBundleImageProvider {
/// Creates an object that fetches the given image from an asset bundle.
///
/// The [name] and [scale] arguments must not be null. The [scale] arguments
/// The [assetName] and [scale] arguments must not be null. The [scale] arguments
/// defaults to 1.0. The [bundle] argument may be null, in which case the
/// bundle provided in the [ImageConfiguration] passed to the [resolve] call
/// will be used instead.
const ExactAssetImage(this.name, {
///
/// The [package] argument must be non-null when fetching an asset that is
/// included in a package. See the documentation for the [ExactAssetImage] class
/// itself for details.
const ExactAssetImage(this.assetName, {
this.scale: 1.0,
this.bundle
}) : assert(name != null),
this.bundle,
this.package,
}) : assert(assetName != null),
assert(scale != null);
/// The name of the asset.
final String assetName;
/// The key to use to obtain the resource from the [bundle]. This is the
/// argument passed to [AssetBundle.load].
final String name;
String get keyName => package == null ? assetName : 'packages/$package/$assetName';
/// The scale to place in the [ImageInfo] object of the image.
final double scale;
......@@ -621,14 +688,18 @@ class ExactAssetImage extends AssetBundleImageProvider {
/// that is also null, the [rootBundle] is used.
///
/// The image is obtained by calling [AssetBundle.load] on the given [bundle]
/// using the key given by [name].
/// using the key given by [keyName].
final AssetBundle bundle;
/// The name of the package from which the image is included. See the
/// documentation for the [ExactAssetImage] class itself for details.
final String package;
@override
Future<AssetBundleImageKey> obtainKey(ImageConfiguration configuration) {
return new SynchronousFuture<AssetBundleImageKey>(new AssetBundleImageKey(
bundle: bundle ?? configuration.bundle ?? rootBundle,
name: name,
name: keyName,
scale: scale
));
}
......@@ -638,14 +709,14 @@ class ExactAssetImage extends AssetBundleImageProvider {
if (other.runtimeType != runtimeType)
return false;
final ExactAssetImage typedOther = other;
return name == typedOther.name
return keyName == typedOther.keyName
&& scale == typedOther.scale
&& bundle == typedOther.bundle;
}
@override
int get hashCode => hashValues(name, scale, bundle);
int get hashCode => hashValues(keyName, scale, bundle);
@override
String toString() => '$runtimeType(name: "$name", scale: $scale, bundle: $bundle)';
String toString() => '$runtimeType(name: "$keyName", scale: $scale, bundle: $bundle)';
}
......@@ -55,16 +55,84 @@ const String _kAssetManifestFileName = 'AssetManifest.json';
/// icons/1.5x/heart.png
/// icons/2.0x/heart.png
/// ```
///
/// ## Fetching assets
///
/// When fetching an image provided by the app itself, use the [assetName]
/// argument to name the asset to choose. For instance, consider the structure
/// above. First, the [pubspec.yaml] of the project should specify its assets in
/// the `flutter` section:
///
/// ```yaml
/// flutter:
/// assets:
/// - icons/heart.png
/// ```
///
/// Then, to fetch the image, use
/// ```dart
/// new AssetImage('icons/heart.png')
/// ```
///
/// ## Assets in packages
///
/// To fetch an asset from a package, the [package] argument must be provided.
/// For instance, suppose the structure above is inside a package called
/// `my_icons`. Then to fetch the image, use:
///
/// ```dart
/// new AssetImage('icons/heart.png', package: 'my_icons')
/// ```
///
/// Assets used by the package itself should also be fetched using the [package]
/// argument as above.
///
/// If the desired asset is specified in the [pubspec.yaml] of the package, it
/// is bundled automatically with the app. In particular, assets used by the
/// package itself must be specified in its [pubspec.yaml].
///
/// A package can also choose to have assets in its 'lib/' folder that are not
/// specified in its [pubspec.yaml]. In this case for those images to be
/// bundled, the app has to specify which ones to include. For instance a
/// package named `fancy_backgrounds` could have:
///
/// ```
/// lib/backgrounds/background1.png
/// lib/backgrounds/background2.png
/// lib/backgrounds/background3.png
///```
///
/// To include, say the first image, the [pubspec.yaml] of the app should specify
/// it in the `assets` section:
///
/// ```yaml
/// assets:
/// - packages/fancy_backgrounds/backgrounds/background1.png
/// ```
///
/// Note that the `lib/` is implied, so it should not be included in the asset
/// path.
///
class AssetImage extends AssetBundleImageProvider {
/// Creates an object that fetches an image from an asset bundle.
///
/// The [name] argument must not be null. It should name the main asset from
/// the set of images to chose from.
const AssetImage(this.name, { this.bundle }) : assert(name != null);
/// The [assetName] argument must not be null. It should name the main asset
/// from the set of images to choose from. The [package] argument must be
/// non-null when fetching an asset that is included in package. See the
/// documentation for the [AssetImage] class itself for details.
const AssetImage(this.assetName, {
this.bundle,
this.package,
}) : assert(assetName != null);
/// The name of the main asset from the set of images to chose from. See the
/// The name of the main asset from the set of images to choose from. See the
/// documentation for the [AssetImage] class itself for details.
final String name;
final String assetName;
/// The name used to generate the key to obtain the asset. For local assets
/// this is [assetName], and for assets from packages the [assetName] is
/// prefixed 'packages/<package_name>/'.
String get keyName => package == null ? assetName : 'packages/$package/$assetName';
/// The bundle from which the image will be obtained.
///
......@@ -73,9 +141,15 @@ class AssetImage extends AssetBundleImageProvider {
/// that is also null, the [rootBundle] is used.
///
/// The image is obtained by calling [AssetBundle.load] on the given [bundle]
/// using the key given by [name].
/// using the key given by [keyName].
final AssetBundle bundle;
/// The name of the package from which the image is included. See the
/// documentation for the [AssetImage] class itself for details.
final String package;
// We assume the main asset is designed for a device pixel ratio of 1.0
static const double _naturalResolution = 1.0;
......@@ -93,9 +167,9 @@ class AssetImage extends AssetBundleImageProvider {
chosenBundle.loadStructuredData<Map<String, List<String>>>(_kAssetManifestFileName, _manifestParser).then<Null>(
(Map<String, List<String>> manifest) {
final String chosenName = _chooseVariant(
name,
keyName,
configuration,
manifest == null ? null : manifest[name]
manifest == null ? null : manifest[keyName]
);
final double chosenScale = _parseScale(chosenName);
final AssetBundleImageKey key = new AssetBundleImageKey(
......@@ -185,13 +259,13 @@ class AssetImage extends AssetBundleImageProvider {
if (other.runtimeType != runtimeType)
return false;
final AssetImage typedOther = other;
return name == typedOther.name
return keyName == typedOther.keyName
&& bundle == typedOther.bundle;
}
@override
int get hashCode => hashValues(name, bundle);
int get hashCode => hashValues(keyName, bundle);
@override
String toString() => '$runtimeType(bundle: $bundle, name: "$name")';
String toString() => '$runtimeType(bundle: $bundle, name: "$keyName")';
}
......@@ -113,7 +113,8 @@ class Image extends StatefulWidget {
this.alignment,
this.repeat: ImageRepeat.noRepeat,
this.centerSlice,
this.gaplessPlayback: false
this.gaplessPlayback: false,
this.package,
}) : assert(image != null),
super(key: key);
......@@ -131,7 +132,8 @@ class Image extends StatefulWidget {
this.alignment,
this.repeat: ImageRepeat.noRepeat,
this.centerSlice,
this.gaplessPlayback: false
this.gaplessPlayback: false,
this.package,
}) : image = new NetworkImage(src, scale: scale),
super(key: key);
......@@ -152,13 +154,18 @@ class Image extends StatefulWidget {
this.alignment,
this.repeat: ImageRepeat.noRepeat,
this.centerSlice,
this.gaplessPlayback: false
this.gaplessPlayback: false,
this.package,
}) : image = new FileImage(file, scale: scale),
super(key: key);
/// Creates a widget that displays an [ImageStream] obtained from an asset
/// bundle. The key for the image is given by the `name` argument.
///
/// The `package` argument must be non-null when displaying an image from a
/// package and null otherwise. See the `Assets in packages` section for
/// details.
///
/// If the `bundle` argument is omitted or null, then the
/// [DefaultAssetBundle] will be used.
///
......@@ -210,6 +217,49 @@ class Image extends StatefulWidget {
/// be present in the manifest). If it is omitted, then on a device with a 1.0
/// device pixel ratio, the `images/2x/cat.png` image would be used instead.
///
///
/// ## Assets in packages
///
/// To create the widget with an asset from a package, the [package] argument
/// must be provided. For instance, suppose a package called `my_icons` has
/// `icons/heart.png` .
///
/// Then to display the image, use:
///
/// ```dart
/// new Image.asset('icons/heart.png', package: 'my_icons')
/// ```
///
/// Assets used by the package itself should also be displayed using the
/// [package] argument as above.
///
/// If the desired asset is specified in the [pubspec.yaml] of the package, it
/// is bundled automatically with the app. In particular, assets used by the
/// package itself must be specified in its [pubspec.yaml].
///
/// A package can also choose to have assets in its 'lib/' folder that are not
/// specified in its [pubspec.yaml]. In this case for those images to be
/// bundled, the app has to specify which ones to include. For instance a
/// package named `fancy_backgrounds` could have:
///
/// ```
/// lib/backgrounds/background1.png
/// lib/backgrounds/background2.png
/// lib/backgrounds/background3.png
///```
///
/// To include, say the first image, the [pubspec.yaml] of the app should
/// specify it in the assets section:
///
/// ```yaml
/// assets:
/// - packages/fancy_backgrounds/backgrounds/background1.png
/// ```
///
/// Note that the `lib/` is implied, so it should not be included in the asset
/// path.
///
///
/// See also:
///
/// * [AssetImage], which is used to implement the behavior when the scale is
......@@ -230,10 +280,12 @@ class Image extends StatefulWidget {
this.alignment,
this.repeat: ImageRepeat.noRepeat,
this.centerSlice,
this.gaplessPlayback: false
}) : image = scale != null ? new ExactAssetImage(name, bundle: bundle, scale: scale)
: new AssetImage(name, bundle: bundle),
super(key: key);
this.gaplessPlayback: false,
this.package,
}) : image = scale != null
? new ExactAssetImage(name, bundle: bundle, scale: scale, package: package)
: new AssetImage(name, bundle: bundle, package: package),
super(key: key);
/// Creates a widget that displays an [ImageStream] obtained from a [Uint8List].
///
......@@ -249,7 +301,8 @@ class Image extends StatefulWidget {
this.alignment,
this.repeat: ImageRepeat.noRepeat,
this.centerSlice,
this.gaplessPlayback: false
this.gaplessPlayback: false,
this.package,
}) : image = new MemoryImage(bytes, scale: scale),
super(key: key);
......@@ -310,6 +363,10 @@ class Image extends StatefulWidget {
/// (false), when the image provider changes.
final bool gaplessPlayback;
/// The name of the package from which the image is included. See the
/// documentation for the [Image.asset] constructor for details.
final String package;
@override
_ImageState createState() => new _ImageState();
......
// Copyright 2017 The Chromium 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/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('AssetImage from package', () {
final AssetImage image = const AssetImage(
'assets/image.png',
package: 'test_package',
);
expect(image.keyName, 'packages/test_package/assets/image.png');
});
test('ExactAssetImage from package', () {
final ExactAssetImage image = const ExactAssetImage(
'assets/image.png',
scale: 1.5,
package: 'test_package',
);
expect(image.keyName, 'packages/test_package/assets/image.png');
});
test('Image.asset from package', () {
final Image imageWidget = new Image.asset(
'assets/image.png',
package: 'test_package',
);
assert(imageWidget.image is AssetImage);
final AssetImage assetImage = imageWidget.image;
expect(assetImage.keyName, 'packages/test_package/assets/image.png');
});
test('Image.asset from package', () {
final Image imageWidget = new Image.asset(
'assets/image.png',
scale: 1.5,
package: 'test_package',
);
assert(imageWidget.image is ExactAssetImage);
final ExactAssetImage asssetImage = imageWidget.image;
expect(asssetImage.keyName, 'packages/test_package/assets/image.png');
});
}
......@@ -86,11 +86,12 @@ class AssetBundle {
return 0;
}
if (manifest != null) {
final int result = await _validateFlutterManifest(manifest);
if (result != 0)
return result;
final int result = await _validateFlutterManifest(manifest);
if (result != 0)
return result;
}
Map<String, dynamic> manifestDescriptor = manifest;
final String appName = manifestDescriptor['name'];
manifestDescriptor = manifestDescriptor['flutter'] ?? <String, dynamic>{};
final String assetBasePath = fs.path.dirname(fs.path.absolute(manifestPath));
......@@ -116,6 +117,33 @@ class AssetBundle {
manifestDescriptor.containsKey('uses-material-design') &&
manifestDescriptor['uses-material-design'];
// Add assets from packages.
for (String packageName in packageMap.map.keys) {
final Uri package = packageMap.map[packageName];
if (package != null && package.scheme == 'file') {
final String packageManifestPath = package.resolve('../pubspec.yaml').path;
final Object packageManifest = _loadFlutterManifest(packageManifestPath);
if (packageManifest == null)
continue;
final int result = await _validateFlutterManifest(packageManifest);
if (result == 0) {
final Map<String, dynamic> packageManifestDescriptor = packageManifest;
// Skip the app itself.
if (packageManifestDescriptor['name'] == appName)
continue;
if (packageManifestDescriptor.containsKey('flutter')) {
final String packageBasePath = fs.path.dirname(packageManifestPath);
assetVariants.addAll(_parseAssets(
packageMap,
packageManifestDescriptor['flutter'],
packageBasePath,
packageKey: packageName,
));
}
}
}
}
// Save the contents of each image, image variant, and font
// asset in entries.
for (_Asset asset in assetVariants.keys) {
......@@ -203,6 +231,27 @@ class _Asset {
@override
String toString() => 'asset: $assetEntry';
@override
bool operator ==(dynamic other) {
if (identical(other, this))
return true;
if (other.runtimeType != runtimeType)
return false;
final _Asset otherAsset = other;
return otherAsset.base == base
&& otherAsset.assetEntry == assetEntry
&& otherAsset.relativePath == relativePath
&& otherAsset.source == source;
}
@override
int get hashCode {
return base.hashCode
^assetEntry.hashCode
^relativePath.hashCode
^ source.hashCode;
}
}
Map<String, dynamic> _readMaterialFontsManifest() {
......@@ -312,8 +361,8 @@ DevFSContent _createAssetManifest(Map<_Asset, List<_Asset>> assetVariants) {
for (_Asset main in assetVariants.keys) {
final List<String> variants = <String>[];
for (_Asset variant in assetVariants[main])
variants.add(variant.relativePath);
json[main.relativePath] = variants;
variants.add(variant.assetEntry);
json[main.assetEntry] = variants;
}
return new DevFSStringContent(JSON.encode(json));
}
......@@ -384,7 +433,8 @@ Map<_Asset, List<_Asset>> _parseAssets(
PackageMap packageMap,
Map<String, dynamic> manifestDescriptor,
String assetBase, {
List<String> excludeDirs: const <String>[]
List<String> excludeDirs: const <String>[],
String packageKey
}) {
final Map<_Asset, List<_Asset>> result = <_Asset, List<_Asset>>{};
......@@ -394,7 +444,9 @@ Map<_Asset, List<_Asset>> _parseAssets(
if (manifestDescriptor.containsKey('assets')) {
final _AssetDirectoryCache cache = new _AssetDirectoryCache(excludeDirs);
for (String assetName in manifestDescriptor['assets']) {
final _Asset asset = _resolveAsset(packageMap, assetBase, assetName);
final _Asset asset = packageKey != null
? _resolvePackageAsset(assetBase, packageKey, assetName)
: _resolveAsset(packageMap, assetBase, assetName);
final List<_Asset> variants = <_Asset>[];
for (String path in cache.variantsFor(asset.assetFile.path)) {
......@@ -435,10 +487,22 @@ Map<_Asset, List<_Asset>> _parseAssets(
return result;
}
_Asset _resolvePackageAsset(
String assetBase,
String packageName,
String asset,
) {
return new _Asset(
base: assetBase,
assetEntry: 'packages/$packageName/$asset',
relativePath: asset,
);
}
_Asset _resolveAsset(
PackageMap packageMap,
String assetBase,
String asset
String asset,
) {
if (asset.startsWith('packages/') && !fs.isFileSync(fs.path.join(assetBase, asset))) {
// Convert packages/flutter_gallery_assets/clouds-0.png to clouds-0.png.
......
// Copyright 2017 The Chromium 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 'dart:async';
import 'dart:convert';
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/asset.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:test/test.dart';
import 'src/common.dart';
import 'src/context.dart';
void main() {
void writePubspecFile(String path, String name, {List<String> assets}) {
String assetsSection;
if (assets == null) {
assetsSection = '';
} else {
final StringBuffer buffer = new StringBuffer();
buffer.write('''
flutter:
assets:
''');
for (String asset in assets) {
buffer.write('''
- $asset
''');
}
assetsSection = buffer.toString();
}
fs.file(path)
..createSync(recursive: true)
..writeAsStringSync('''
name: $name
dependencies:
flutter:
sdk: flutter
$assetsSection
''');
}
void establishFlutterRoot() {
// Setting flutterRoot here so that it picks up the MemoryFileSystem's
// path separator.
Cache.flutterRoot = getFlutterRoot();
}
void writePackagesFile(String packages) {
fs.file(".packages")
..createSync()
..writeAsStringSync(packages);
}
Future<Null> buildAndVerifyAssets(
List<String> assets,
List<String> packages,
String expectedAssetManifest,
) async {
final AssetBundle bundle = new AssetBundle();
await bundle.build(manifestPath: 'pubspec.yaml');
for (String packageName in packages) {
for (String asset in assets) {
final String entryKey = 'packages/$packageName/$asset';
expect(bundle.entries.containsKey(entryKey), true);
expect(
UTF8.decode(await bundle.entries[entryKey].contentsAsBytes()),
asset,
);
}
}
expect(
UTF8.decode(await bundle.entries['AssetManifest.json'].contentsAsBytes()),
expectedAssetManifest,
);
}
void writeAssets(String path, List<String> assets) {
for (String asset in assets) {
fs.file('$path$asset')
..createSync(recursive: true)
..writeAsStringSync(asset);
}
}
group('AssetBundle assets from package', () {
testUsingContext('One package with no assets', () async {
establishFlutterRoot();
writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
writePubspecFile('p/p/pubspec.yaml', 'test_package');
final AssetBundle bundle = new AssetBundle();
await bundle.build(manifestPath: 'pubspec.yaml');
expect(bundle.entries.length, 2); // LICENSE, AssetManifest
}, overrides: <Type, Generator>{
FileSystem: () => new MemoryFileSystem(),
});
testUsingContext('One package with one asset', () async {
establishFlutterRoot();
writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
final List<String> assets = <String>['a/foo'];
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
assets: assets,
);
writeAssets('p/p/', assets);
final String expectedAssetManifest = '{"packages/test_package/a/foo":'
'["packages/test_package/a/foo"]}';
await buildAndVerifyAssets(
assets,
<String>['test_package'],
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => new MemoryFileSystem(),
});
testUsingContext('One package with asset variants', () async {
establishFlutterRoot();
writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
assets: <String>['a/foo'],
);
final List<String> assets = <String>['a/foo', 'a/v/foo'];
writeAssets('p/p/', assets);
final String expectedManifest = '{"packages/test_package/a/foo":'
'["packages/test_package/a/foo","packages/test_package/a/v/foo"]}';
await buildAndVerifyAssets(
assets,
<String>['test_package'],
expectedManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => new MemoryFileSystem(),
});
testUsingContext('One package with two assets', () async {
establishFlutterRoot();
writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
final List<String> assets = <String>['a/foo', 'a/bar'];
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
assets: assets,
);
writeAssets('p/p/', assets);
final String expectedAssetManifest =
'{"packages/test_package/a/foo":["packages/test_package/a/foo"],'
'"packages/test_package/a/bar":["packages/test_package/a/bar"]}';
await buildAndVerifyAssets(
assets,
<String>['test_package'],
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => new MemoryFileSystem(),
});
testUsingContext('Two packages with assets', () async {
establishFlutterRoot();
writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/\ntest_package2:p2/p/lib/');
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
assets: <String>['a/foo'],
);
writePubspecFile(
'p2/p/pubspec.yaml',
'test_package2',
assets: <String>['a/foo'],
);
final List<String> assets = <String>['a/foo', 'a/v/foo'];
writeAssets('p/p/', assets);
writeAssets('p2/p/', assets);
final String expectedAssetManifest =
'{"packages/test_package/a/foo":'
'["packages/test_package/a/foo","packages/test_package/a/v/foo"],'
'"packages/test_package2/a/foo":'
'["packages/test_package2/a/foo","packages/test_package2/a/v/foo"]}';
await buildAndVerifyAssets(
assets,
<String>['test_package', 'test_package2'],
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => new MemoryFileSystem(),
});
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment