Write snippets index file when generating docs (#25515)

diff --git a/dev/snippets/lib/main.dart b/dev/snippets/lib/main.dart
index 3f47559..997e442 100644
--- a/dev/snippets/lib/main.dart
+++ b/dev/snippets/lib/main.dart
@@ -52,6 +52,7 @@
     defaultsTo: null,
     help: 'The output path for the generated snippet application. Overrides '
         'the naming generated by the --package/--library/--element arguments. '
+        'Metadata will be written alongside in a .json file. '
         'The basename of this argument is used as the ID',
   );
   parser.addOption(
@@ -113,20 +114,21 @@
     template = args[_kTemplateOption].toString().replaceAll(RegExp(r'.tmpl$'), '');
   }
 
+  final String packageName = args[_kPackageOption] != null && args[_kPackageOption].isNotEmpty ? args[_kPackageOption] : null;
+  final String libraryName = args[_kLibraryOption] != null && args[_kLibraryOption].isNotEmpty ? args[_kLibraryOption] : null;
+  final String elementName = args[_kElementOption] != null && args[_kElementOption].isNotEmpty ? args[_kElementOption] : null;
   final List<String> id = <String>[];
   if (args[_kOutputOption] != null) {
     id.add(path.basename(path.basenameWithoutExtension(args[_kOutputOption])));
   } else {
-    if (args[_kPackageOption] != null &&
-        args[_kPackageOption].isNotEmpty &&
-        args[_kPackageOption] != 'flutter') {
-      id.add(args[_kPackageOption]);
+    if (packageName != null && packageName != 'flutter') {
+      id.add(packageName);
     }
-    if (args[_kLibraryOption] != null && args[_kLibraryOption].isNotEmpty) {
-      id.add(args[_kLibraryOption]);
+    if (libraryName != null) {
+      id.add(libraryName);
     }
-    if (args[_kElementOption] != null && args[_kElementOption].isNotEmpty) {
-      id.add(args[_kElementOption]);
+    if (elementName != null) {
+      id.add(elementName);
     }
     if (id.isEmpty) {
       errorExit('Unable to determine ID. At least one of --$_kPackageOption, '
@@ -142,6 +144,13 @@
     template: template,
     id: id.join('.'),
     output: args[_kOutputOption] != null ? File(args[_kOutputOption]) : null,
+    metadata: <String, Object>{
+      'sourcePath': environment['SOURCE_PATH'],
+      'package': packageName,
+      'library': libraryName,
+      'element': elementName,
+    },
   ));
+
   exit(0);
 }
diff --git a/dev/snippets/lib/snippets.dart b/dev/snippets/lib/snippets.dart
index cb3201c..0881694 100644
--- a/dev/snippets/lib/snippets.dart
+++ b/dev/snippets/lib/snippets.dart
@@ -39,6 +39,8 @@
   /// snippet.
   final Configuration configuration;
 
+  static const JsonEncoder jsonEncoder = JsonEncoder.withIndent('    ');
+
   /// A Dart formatted used to format the snippet code and finished application
   /// code.
   static DartFormatter formatter = DartFormatter(pageWidth: 80, fixes: StyleFix.all);
@@ -193,7 +195,7 @@
   /// The [id] is a string ID to use for the output file, and to tell the user
   /// about in the `flutter create` hint. It must not be null if the [type] is
   /// [SnippetType.application].
-  String generate(File input, SnippetType type, {String template, String id, File output}) {
+  String generate(File input, SnippetType type, {String template, String id, File output, Map<String, Object> metadata}) {
     assert(template != null || type != SnippetType.application);
     assert(id != null || type != SnippetType.application);
     assert(input != null);
@@ -226,6 +228,23 @@
         final File outputFile = output ?? getOutputFile(id);
         stderr.writeln('Writing to ${outputFile.absolute.path}');
         outputFile.writeAsStringSync(app);
+
+        final File metadataFile = File(path.join(path.dirname(outputFile.path),
+            '${path.basenameWithoutExtension(outputFile.path)}.json'));
+        stderr.writeln('Writing metadata to ${metadataFile.absolute.path}');
+        final _ComponentTuple description = snippetData.firstWhere(
+          (_ComponentTuple data) => data.name == 'description',
+          orElse: () => null,
+        );
+        metadata ??= <String, Object>{};
+        metadata.addAll(<String, Object>{
+          'id': id,
+          'file': path.basename(outputFile.path),
+          'description': description != null
+              ? description.mergedContent
+              : null,
+        });
+        metadataFile.writeAsStringSync(jsonEncoder.convert(metadata));
         break;
       case SnippetType.sample:
         break;
diff --git a/dev/snippets/test/snippets_test.dart b/dev/snippets/test/snippets_test.dart
index c73e38d..9b1a07b 100644
--- a/dev/snippets/test/snippets_test.dart
+++ b/dev/snippets/test/snippets_test.dart
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:convert';
 import 'dart:io' hide Platform;
 import 'package:path/path.dart' as path;
 
@@ -111,5 +112,40 @@
           'On several lines.{@inject-html}</div>\n'));
       expect(html, contains('main() {'));
     });
+
+    test('generates snippet application metadata', () async {
+      final File inputFile = File(path.join(tmpDir.absolute.path, 'snippet_in.txt'))
+        ..createSync(recursive: true)
+        ..writeAsStringSync('''
+A description of the snippet.
+
+On several lines.
+
+```code
+void main() {
+  print('The actual \$name.');
+}
+```
+''');
+
+      final File outputFile = File(path.join(tmpDir.absolute.path, 'snippet_out.dart'));
+      final File expectedMetadataFile = File(path.join(tmpDir.absolute.path, 'snippet_out.json'));
+
+      generator.generate(
+        inputFile,
+        SnippetType.application,
+        template: 'template',
+        id: 'id',
+        output: outputFile,
+        metadata: <String, Object>{'sourcePath': 'some/path.dart'},
+      );
+      expect(expectedMetadataFile.existsSync(), isTrue);
+      final Map<String, dynamic> json = jsonDecode(expectedMetadataFile.readAsStringSync());
+      expect(json['id'], equals('id'));
+      expect(json['file'], equals('snippet_out.dart'));
+      expect(json['description'], equals('A description of the snippet.\n\nOn several lines.'));
+      // Ensure any passed metadata is included in the output JSON too.
+      expect(json['sourcePath'], equals('some/path.dart'));
+    });
   });
 }
diff --git a/dev/tools/dartdoc.dart b/dev/tools/dartdoc.dart
index 2c259cf..de3751c 100644
--- a/dev/tools/dartdoc.dart
+++ b/dev/tools/dartdoc.dart
@@ -353,6 +353,7 @@
   addHtmlBaseToIndex();
   changePackageToSdkInTitlebar();
   putRedirectInOldIndexLocation();
+  writeSnippetsIndexFile();
   print('\nDocs ready to go!');
 }
 
@@ -407,6 +408,23 @@
   File('$kPublishRoot/flutter/index.html').writeAsStringSync(metaTag);
 }
 
+
+void writeSnippetsIndexFile() {
+  final Directory snippetsDir = Directory(path.join(kPublishRoot, 'snippets'));
+  if (snippetsDir.existsSync()) {
+    const JsonEncoder jsonEncoder = JsonEncoder.withIndent('    ');
+    final Iterable<File> files = snippetsDir
+        .listSync()
+        .whereType<File>()
+        .where((File file) => path.extension(file.path) == '.json');
+        // Combine all the metadata into a single JSON array.
+    final Iterable<String> fileContents = files.map((File file) => file.readAsStringSync());
+    final List<dynamic> metadataObjects = fileContents.map<dynamic>(json.decode).toList();
+    final String jsonArray = jsonEncoder.convert(metadataObjects);
+    File('$kPublishRoot/snippets/index.json').writeAsStringSync(jsonArray);
+  }
+}
+
 List<String> findPackageNames() {
   return findPackages().map<String>((FileSystemEntity file) => path.basename(file.path)).toList();
 }