pubspec_parse: Added support for `executables` field in `pubspec.yaml` (#1952)

Co-authored-by: Kevin Moore <kevmoo@users.noreply.github.com>
diff --git a/pkgs/pubspec_parse/CHANGELOG.md b/pkgs/pubspec_parse/CHANGELOG.md
index 124b5e4..5aeb498 100644
--- a/pkgs/pubspec_parse/CHANGELOG.md
+++ b/pkgs/pubspec_parse/CHANGELOG.md
@@ -1,6 +1,6 @@
-## 1.5.0-wip
+## 1.5.0
 
-- Add `Pubspec.workspace` and `Pubspec.resolution` fields.
+- Added fields to `Pubspec`: `executables`, `resolution`, `workspace`.
 - Require Dart 3.6
 - Update dependencies.
 
diff --git a/pkgs/pubspec_parse/lib/src/pubspec.dart b/pkgs/pubspec_parse/lib/src/pubspec.dart
index cdfc8ff..eb77908 100644
--- a/pkgs/pubspec_parse/lib/src/pubspec.dart
+++ b/pkgs/pubspec_parse/lib/src/pubspec.dart
@@ -93,6 +93,10 @@
   /// and other settings.
   final Map<String, dynamic>? flutter;
 
+  /// Optional field to specify executables
+  @JsonKey(fromJson: _executablesMap)
+  final Map<String, String?> executables;
+
   /// If this package is a Pub Workspace, this field lists the sub-packages.
   final List<String>? workspace;
 
@@ -129,12 +133,14 @@
     Map<String, Dependency>? devDependencies,
     Map<String, Dependency>? dependencyOverrides,
     this.flutter,
+    Map<String, String?>? executables,
   })  :
         // ignore: deprecated_member_use_from_same_package
         authors = _normalizeAuthors(author, authors),
         environment = environment ?? const {},
         dependencies = dependencies ?? const {},
         devDependencies = devDependencies ?? const {},
+        executables = executables ?? const {},
         dependencyOverrides = dependencyOverrides ?? const {} {
     if (name.isEmpty) {
       throw ArgumentError.value(name, 'name', '"name" cannot be empty.');
@@ -232,3 +238,21 @@
       return MapEntry(key, constraint);
     }) ??
     {};
+
+Map<String, String?> _executablesMap(Map? source) =>
+    source?.map((k, value) {
+      final key = k as String;
+      if (value == null) {
+        return MapEntry(key, null);
+      } else if (value is String) {
+        return MapEntry(key, value);
+      } else {
+        throw CheckedFromJsonException(
+          source,
+          key,
+          'String',
+          '`$value` is not a String.',
+        );
+      }
+    }) ??
+    {};
diff --git a/pkgs/pubspec_parse/lib/src/pubspec.g.dart b/pkgs/pubspec_parse/lib/src/pubspec.g.dart
index f0fb79e..58e015a 100644
--- a/pkgs/pubspec_parse/lib/src/pubspec.g.dart
+++ b/pkgs/pubspec_parse/lib/src/pubspec.g.dart
@@ -54,6 +54,8 @@
               (v) => (v as Map?)?.map(
                     (k, e) => MapEntry(k as String, e),
                   )),
+          executables:
+              $checkedConvert('executables', (v) => _executablesMap(v as Map?)),
         );
         return val;
       },
diff --git a/pkgs/pubspec_parse/pubspec.yaml b/pkgs/pubspec_parse/pubspec.yaml
index 6bf96bb..73a1117 100644
--- a/pkgs/pubspec_parse/pubspec.yaml
+++ b/pkgs/pubspec_parse/pubspec.yaml
@@ -1,5 +1,5 @@
 name: pubspec_parse
-version: 1.5.0-wip
+version: 1.5.0
 description: >-
   Simple package for parsing pubspec.yaml files with a type-safe API and rich
   error reporting.
diff --git a/pkgs/pubspec_parse/test/parse_test.dart b/pkgs/pubspec_parse/test/parse_test.dart
index 5570b60..e0698af 100644
--- a/pkgs/pubspec_parse/test/parse_test.dart
+++ b/pkgs/pubspec_parse/test/parse_test.dart
@@ -34,6 +34,7 @@
     expect(value.screenshots, isEmpty);
     expect(value.workspace, isNull);
     expect(value.resolution, isNull);
+    expect(value.executables, isEmpty);
   });
 
   test('all fields set', () async {
@@ -64,6 +65,10 @@
           'pkg2',
         ],
         'resolution': 'workspace',
+        'executables': {
+          'my_script': 'bin/my_script.dart',
+          'my_script2': 'bin/my_script2.dart',
+        },
       },
       skipTryPub: true,
     );
@@ -96,6 +101,11 @@
     expect(value.screenshots, hasLength(1));
     expect(value.screenshots!.first.description, 'my screenshot');
     expect(value.screenshots!.first.path, 'path/to/screenshot');
+    expect(value.executables, hasLength(2));
+    expect(value.executables.keys, contains('my_script'));
+    expect(value.executables.keys, contains('my_script2'));
+    expect(value.executables['my_script'], 'bin/my_script.dart');
+    expect(value.executables['my_script2'], 'bin/my_script2.dart');
     expect(value.workspace, hasLength(2));
     expect(value.workspace!.first, 'pkg1');
     expect(value.workspace!.last, 'pkg2');
@@ -222,6 +232,58 @@
     });
   });
 
+  group('executables', () {
+    test('one executable', () async {
+      final value = await parse({
+        ...defaultPubspec,
+        'executables': {'my_script': 'bin/my_script.dart'},
+      });
+      expect(value.executables, hasLength(1));
+      expect(value.executables.keys, contains('my_script'));
+      expect(value.executables['my_script'], 'bin/my_script.dart');
+    });
+
+    test('many executables', () async {
+      final value = await parse({
+        ...defaultPubspec,
+        'executables': {
+          'my_script': 'bin/my_script.dart',
+          'my_script2': 'bin/my_script2.dart',
+        },
+      });
+      expect(value.executables, hasLength(2));
+      expect(value.executables.keys, contains('my_script'));
+      expect(value.executables.keys, contains('my_script2'));
+      expect(value.executables['my_script'], 'bin/my_script.dart');
+      expect(value.executables['my_script2'], 'bin/my_script2.dart');
+    });
+
+    test('invalid value', () async {
+      expectParseThrowsContaining(
+        {
+          ...defaultPubspec,
+          'executables': {
+            'script': 32,
+          },
+        },
+        'Unsupported value for "script". `32` is not a String.',
+        skipTryPub: true,
+      );
+    });
+
+    test('invalid executable - lenient', () async {
+      final value = await parse(
+        {
+          ...defaultPubspec,
+          'executables': 'Invalid value',
+        },
+        lenient: true,
+      );
+      expect(value.name, 'sample');
+      expect(value.executables, isEmpty);
+    });
+  });
+
   group('invalid', () {
     test('null', () {
       expectParseThrows(