Watch wildcard directories in addition to asset bundle (#29883)

diff --git a/packages/flutter_tools/lib/src/asset.dart b/packages/flutter_tools/lib/src/asset.dart
index e845551..267be6a 100644
--- a/packages/flutter_tools/lib/src/asset.dart
+++ b/packages/flutter_tools/lib/src/asset.dart
@@ -63,6 +63,10 @@
   @override
   final Map<String, DevFSContent> entries = <String, DevFSContent>{};
 
+  // If an asset corresponds to a wildcard directory, then it may have been
+  // updated without changes to the manifest.
+  final Map<Uri, Directory> _wildcardDirectories = <Uri, Directory>{};
+
   DateTime _lastBuildTimestamp;
 
   static const String defaultManifestPath = 'pubspec.yaml';
@@ -83,6 +87,12 @@
     if (stat.type == FileSystemEntityType.notFound)
       return true;
 
+    for (Directory directory in _wildcardDirectories.values) {
+      if (directory.statSync().modified.isAfter(_lastBuildTimestamp)) {
+        return true;
+      }
+    }
+
     return stat.modified.isAfter(_lastBuildTimestamp);
   }
 
@@ -119,6 +129,7 @@
     final String assetBasePath = fs.path.dirname(fs.path.absolute(manifestPath));
 
     final PackageMap packageMap = PackageMap(packagesPath);
+    final List<Uri> wildcardDirectories = <Uri>[];
 
     // The _assetVariants map contains an entry for each asset listed
     // in the pubspec.yaml file's assets and font and sections. The
@@ -127,12 +138,14 @@
     final Map<_Asset, List<_Asset>> assetVariants = _parseAssets(
       packageMap,
       flutterManifest,
+      wildcardDirectories,
       assetBasePath,
       excludeDirs: <String>[assetDirPath, getBuildDirectory()],
     );
 
-    if (assetVariants == null)
+    if (assetVariants == null) {
       return 1;
+    }
 
     final List<Map<String, dynamic>> fonts = _parseFonts(
       flutterManifest,
@@ -156,6 +169,7 @@
         final Map<_Asset, List<_Asset>> packageAssets = _parseAssets(
           packageMap,
           packageFlutterManifest,
+          wildcardDirectories,
           packageBasePath,
           packageName: packageName,
         );
@@ -206,6 +220,11 @@
       entries[asset.entryUri.path] ??= DevFSFileContent(asset.assetFile);
     }
 
+    // Update wildcard directories we we can detect changes in them.
+    for (Uri uri in wildcardDirectories) {
+      _wildcardDirectories[uri] ??= fs.directory(uri);
+    }
+
     entries[_assetManifestJson] = _createAssetManifest(assetVariants);
 
     entries[_fontManifestJson] = DevFSStringContent(json.encode(fonts));
@@ -524,6 +543,7 @@
 Map<_Asset, List<_Asset>> _parseAssets(
   PackageMap packageMap,
   FlutterManifest flutterManifest,
+  List<Uri> wildcardDirectories,
   String assetBase, {
   List<String> excludeDirs = const <String>[],
   String packageName,
@@ -533,6 +553,7 @@
   final _AssetDirectoryCache cache = _AssetDirectoryCache(excludeDirs);
   for (Uri assetUri in flutterManifest.assets) {
     if (assetUri.toString().endsWith('/')) {
+      wildcardDirectories.add(assetUri);
       _parseAssetsFromFolder(packageMap, flutterManifest, assetBase,
           cache, result, assetUri,
           excludeDirs: excludeDirs, packageName: packageName);
diff --git a/packages/flutter_tools/test/asset_bundle_test.dart b/packages/flutter_tools/test/asset_bundle_test.dart
index 7a0bb00..5dc16e3 100644
--- a/packages/flutter_tools/test/asset_bundle_test.dart
+++ b/packages/flutter_tools/test/asset_bundle_test.dart
@@ -56,6 +56,46 @@
     }, overrides: <Type, Generator>{
       FileSystem: () => testFileSystem,
     });
+
+    testUsingContext('wildcard directories are updated when filesystem changes', () async {
+      fs.file('.packages').createSync();
+      fs.file(fs.path.join('assets', 'foo', 'bar.txt')).createSync(recursive: true);
+      fs.file('pubspec.yaml')
+        ..createSync()
+        ..writeAsStringSync(r'''
+name: example
+flutter:
+  assets:
+    - assets/foo/
+''');
+      final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
+      await bundle.build(manifestPath: 'pubspec.yaml');
+      // Expected assets:
+      //  - asset manifest
+      //  - font manifest
+      //  - license file
+      //  - assets/foo/bar.txt
+      expect(bundle.entries.length, 4);
+      expect(bundle.needsBuild(manifestPath: 'pubspec.yaml'), false);
+
+      // Adding a file should update the stat of the directory, but instead
+      // we need to fully recreate it.
+      fs.directory(fs.path.join('assets', 'foo')).deleteSync(recursive: true);
+      fs.file(fs.path.join('assets', 'foo', 'fizz.txt')).createSync(recursive: true);
+      fs.file(fs.path.join('assets', 'foo', 'bar.txt')).createSync();
+
+      expect(bundle.needsBuild(manifestPath: 'pubspec.yaml'), true);
+      await bundle.build(manifestPath: 'pubspec.yaml');
+      // Expected assets:
+      //  - asset manifest
+      //  - font manifest
+      //  - license file
+      //  - assets/foo/bar.txt
+      //  - assets/foo/fizz.txt
+      expect(bundle.entries.length, 5);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    });
   });
 
 }