| // Copyright 2014 The Flutter 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:meta/meta.dart'; |
| import 'package:pub_semver/pub_semver.dart'; |
| import 'package:yaml/yaml.dart'; |
| |
| import 'base/deferred_component.dart'; |
| import 'base/file_system.dart'; |
| import 'base/logger.dart'; |
| import 'base/utils.dart'; |
| import 'globals.dart' as globals; |
| import 'plugins.dart'; |
| |
| /// Whether or not Impeller Scene 3D model import is enabled. |
| const bool kIs3dSceneSupported = true; |
| |
| const Set<String> _kValidPluginPlatforms = <String>{ |
| 'android', 'ios', 'web', 'windows', 'linux', 'macos', |
| }; |
| |
| /// A wrapper around the `flutter` section in the `pubspec.yaml` file. |
| class FlutterManifest { |
| FlutterManifest._({required Logger logger}) : _logger = logger; |
| |
| /// Returns an empty manifest. |
| factory FlutterManifest.empty({ required Logger logger }) = FlutterManifest._; |
| |
| /// Returns null on invalid manifest. Returns empty manifest on missing file. |
| static FlutterManifest? createFromPath(String path, { |
| required FileSystem fileSystem, |
| required Logger logger, |
| }) { |
| if (!fileSystem.isFileSync(path)) { |
| return _createFromYaml(null, logger); |
| } |
| final String manifest = fileSystem.file(path).readAsStringSync(); |
| return FlutterManifest.createFromString(manifest, logger: logger); |
| } |
| |
| /// Returns null on missing or invalid manifest. |
| @visibleForTesting |
| static FlutterManifest? createFromString(String manifest, { required Logger logger }) { |
| return _createFromYaml(loadYaml(manifest), logger); |
| } |
| |
| static FlutterManifest? _createFromYaml(Object? yamlDocument, Logger logger) { |
| if (yamlDocument != null && !_validate(yamlDocument, logger)) { |
| return null; |
| } |
| |
| final FlutterManifest pubspec = FlutterManifest._(logger: logger); |
| final Map<Object?, Object?>? yamlMap = yamlDocument as YamlMap?; |
| if (yamlMap != null) { |
| pubspec._descriptor = yamlMap.cast<String, Object?>(); |
| } |
| |
| final Map<Object?, Object?>? flutterMap = pubspec._descriptor['flutter'] as Map<Object?, Object?>?; |
| if (flutterMap != null) { |
| pubspec._flutterDescriptor = flutterMap.cast<String, Object?>(); |
| } |
| |
| return pubspec; |
| } |
| |
| final Logger _logger; |
| |
| /// A map representation of the entire `pubspec.yaml` file. |
| Map<String, Object?> _descriptor = <String, Object?>{}; |
| |
| /// A map representation of the `flutter` section in the `pubspec.yaml` file. |
| Map<String, Object?> _flutterDescriptor = <String, Object?>{}; |
| |
| Map<String, Object?> get flutterDescriptor => _flutterDescriptor; |
| |
| /// True if the `pubspec.yaml` file does not exist. |
| bool get isEmpty => _descriptor.isEmpty; |
| |
| /// The string value of the top-level `name` property in the `pubspec.yaml` file. |
| String get appName => _descriptor['name'] as String? ?? ''; |
| |
| /// Contains the name of the dependencies. |
| /// These are the keys specified in the `dependency` map. |
| Set<String> get dependencies { |
| final YamlMap? dependencies = _descriptor['dependencies'] as YamlMap?; |
| return dependencies != null ? <String>{...dependencies.keys.cast<String>()} : <String>{}; |
| } |
| |
| // Flag to avoid printing multiple invalid version messages. |
| bool _hasShowInvalidVersionMsg = false; |
| |
| /// The version String from the `pubspec.yaml` file. |
| /// Can be null if it isn't set or has a wrong format. |
| String? get appVersion { |
| final String? verStr = _descriptor['version']?.toString(); |
| if (verStr == null) { |
| return null; |
| } |
| |
| Version? version; |
| try { |
| version = Version.parse(verStr); |
| } on Exception { |
| if (!_hasShowInvalidVersionMsg) { |
| _logger.printStatus(globals.userMessages.invalidVersionSettingHintMessage(verStr), emphasis: true); |
| _hasShowInvalidVersionMsg = true; |
| } |
| } |
| return version?.toString(); |
| } |
| |
| /// The build version name from the `pubspec.yaml` file. |
| /// Can be null if version isn't set or has a wrong format. |
| String? get buildName { |
| final String? version = appVersion; |
| if (version != null && version.contains('+')) { |
| return version.split('+').elementAt(0); |
| } |
| return version; |
| } |
| |
| /// The build version number from the `pubspec.yaml` file. |
| /// Can be null if version isn't set or has a wrong format. |
| String? get buildNumber { |
| final String? version = appVersion; |
| if (version != null && version.contains('+')) { |
| final String value = version.split('+').elementAt(1); |
| return value; |
| } else { |
| return null; |
| } |
| } |
| |
| bool get usesMaterialDesign { |
| return _flutterDescriptor['uses-material-design'] as bool? ?? false; |
| } |
| |
| /// If true, does not use Swift Package Manager as a dependency manager. |
| /// CocoaPods will be used instead. |
| bool get disabledSwiftPackageManager { |
| return _flutterDescriptor['disable-swift-package-manager'] as bool? ?? false; |
| } |
| |
| /// True if this Flutter module should use AndroidX dependencies. |
| /// |
| /// If false the deprecated Android Support library will be used. |
| bool get usesAndroidX { |
| final Object? module = _flutterDescriptor['module']; |
| if (module is YamlMap) { |
| return module['androidX'] == true; |
| } |
| return false; |
| } |
| |
| /// Any additional license files listed under the `flutter` key. |
| /// |
| /// This is expected to be a list of file paths that should be treated as |
| /// relative to the pubspec in this directory. |
| /// |
| /// For example: |
| /// |
| /// ```yaml |
| /// flutter: |
| /// licenses: |
| /// - assets/foo_license.txt |
| /// ``` |
| List<String> get additionalLicenses { |
| final Object? licenses = _flutterDescriptor['licenses']; |
| if (licenses is YamlList) { |
| return licenses.map((Object? element) => element.toString()).toList(); |
| } |
| return <String>[]; |
| } |
| |
| /// True if this manifest declares a Flutter module project. |
| /// |
| /// A Flutter project is considered a module when it has a `module:` |
| /// descriptor. A Flutter module project supports integration into an |
| /// existing host app, and has managed platform host code. |
| /// |
| /// Such a project can be created using `flutter create -t module`. |
| bool get isModule => _flutterDescriptor.containsKey('module'); |
| |
| /// True if this manifest declares a Flutter plugin project. |
| /// |
| /// A Flutter project is considered a plugin when it has a `plugin:` |
| /// descriptor. A Flutter plugin project wraps custom Android and/or |
| /// iOS code in a Dart interface for consumption by other Flutter app |
| /// projects. |
| /// |
| /// Such a project can be created using `flutter create -t plugin`. |
| bool get isPlugin => _flutterDescriptor.containsKey('plugin'); |
| |
| /// Returns the Android package declared by this manifest in its |
| /// module or plugin descriptor. Returns null, if there is no |
| /// such declaration. |
| String? get androidPackage { |
| if (isModule) { |
| final Object? module = _flutterDescriptor['module']; |
| if (module is YamlMap) { |
| return module['androidPackage'] as String?; |
| } |
| } |
| final Map<String, Object?>? platforms = supportedPlatforms; |
| if (platforms == null) { |
| // Pre-multi-platform plugin format |
| if (isPlugin) { |
| final YamlMap? plugin = _flutterDescriptor['plugin'] as YamlMap?; |
| return plugin?['androidPackage'] as String?; |
| } |
| return null; |
| } |
| if (platforms.containsKey('android')) { |
| final Object? android = platforms['android']; |
| if (android is YamlMap) { |
| return android['package'] as String?; |
| } |
| } |
| return null; |
| } |
| |
| /// Returns the deferred components configuration if declared. Returns |
| /// null if no deferred components are declared. |
| late final List<DeferredComponent>? deferredComponents = computeDeferredComponents(); |
| List<DeferredComponent>? computeDeferredComponents() { |
| if (!_flutterDescriptor.containsKey('deferred-components')) { |
| return null; |
| } |
| final List<DeferredComponent> components = <DeferredComponent>[]; |
| final Object? deferredComponents = _flutterDescriptor['deferred-components']; |
| if (deferredComponents is! YamlList) { |
| return components; |
| } |
| for (final Object? component in deferredComponents) { |
| if (component is! YamlMap) { |
| _logger.printError('Expected deferred component manifest to be a map.'); |
| continue; |
| } |
| components.add( |
| DeferredComponent( |
| name: component['name'] as String, |
| libraries: component['libraries'] == null ? |
| <String>[] : (component['libraries'] as List<dynamic>).cast<String>(), |
| assets: _computeAssets(component['assets']), |
| ) |
| ); |
| } |
| return components; |
| } |
| |
| /// Returns the iOS bundle identifier declared by this manifest in its |
| /// module descriptor. Returns null if there is no such declaration. |
| String? get iosBundleIdentifier { |
| if (isModule) { |
| final Object? module = _flutterDescriptor['module']; |
| if (module is YamlMap) { |
| return module['iosBundleIdentifier'] as String?; |
| } |
| } |
| return null; |
| } |
| |
| /// Gets the supported platforms. This only supports the new `platforms` format. |
| /// |
| /// If the plugin uses the legacy pubspec format, this method returns null. |
| Map<String, Object?>? get supportedPlatforms { |
| if (isPlugin) { |
| final YamlMap? plugin = _flutterDescriptor['plugin'] as YamlMap?; |
| if (plugin?.containsKey('platforms') ?? false) { |
| final YamlMap? platformsMap = plugin!['platforms'] as YamlMap?; |
| return platformsMap?.value.cast<String, Object?>(); |
| } |
| } |
| return null; |
| } |
| |
| /// Like [supportedPlatforms], but only returns the valid platforms that are supported in flutter plugins. |
| Map<String, Object?>? get validSupportedPlatforms { |
| final Map<String, Object?>? allPlatforms = supportedPlatforms; |
| if (allPlatforms == null) { |
| return null; |
| } |
| final Map<String, Object?> platforms = <String, Object?>{}..addAll(allPlatforms); |
| platforms.removeWhere((String key, Object? _) => !_kValidPluginPlatforms.contains(key)); |
| if (platforms.isEmpty) { |
| return null; |
| } |
| return platforms; |
| } |
| |
| List<Map<String, Object?>> get fontsDescriptor { |
| return fonts.map((Font font) => font.descriptor).toList(); |
| } |
| |
| List<Map<String, Object?>> get _rawFontsDescriptor { |
| final List<Object?>? fontList = _flutterDescriptor['fonts'] as List<Object?>?; |
| return fontList == null |
| ? const <Map<String, Object?>>[] |
| : fontList.map<Map<String, Object?>?>(castStringKeyedMap).whereType<Map<String, Object?>>().toList(); |
| } |
| |
| late final List<AssetsEntry> assets = _computeAssets(_flutterDescriptor['assets']); |
| |
| late final List<Font> fonts = _extractFonts(); |
| |
| List<Font> _extractFonts() { |
| if (!_flutterDescriptor.containsKey('fonts')) { |
| return <Font>[]; |
| } |
| |
| final List<Font> fonts = <Font>[]; |
| for (final Map<String, Object?> fontFamily in _rawFontsDescriptor) { |
| final YamlList? fontFiles = fontFamily['fonts'] as YamlList?; |
| final String? familyName = fontFamily['family'] as String?; |
| if (familyName == null) { |
| _logger.printWarning('Warning: Missing family name for font.', emphasis: true); |
| continue; |
| } |
| if (fontFiles == null) { |
| _logger.printWarning('Warning: No fonts specified for font $familyName', emphasis: true); |
| continue; |
| } |
| |
| final List<FontAsset> fontAssets = <FontAsset>[]; |
| for (final Map<Object?, Object?> fontFile in fontFiles.cast<Map<Object?, Object?>>()) { |
| final String? asset = fontFile['asset'] as String?; |
| if (asset == null) { |
| _logger.printWarning('Warning: Missing asset in fonts for $familyName', emphasis: true); |
| continue; |
| } |
| |
| fontAssets.add(FontAsset( |
| Uri.parse(asset), |
| weight: fontFile['weight'] as int?, |
| style: fontFile['style'] as String?, |
| )); |
| } |
| if (fontAssets.isNotEmpty) { |
| fonts.add(Font(familyName, fontAssets)); |
| } |
| } |
| return fonts; |
| } |
| |
| late final List<Uri> shaders = _extractAssetUris('shaders', 'Shader'); |
| late final List<Uri> models = kIs3dSceneSupported ? _extractAssetUris('models', 'Model') : <Uri>[]; |
| |
| List<Uri> _extractAssetUris(String key, String singularName) { |
| if (!_flutterDescriptor.containsKey(key)) { |
| return <Uri>[]; |
| } |
| |
| final List<Object?>? items = _flutterDescriptor[key] as List<Object?>?; |
| if (items == null) { |
| return const <Uri>[]; |
| } |
| final List<Uri> results = <Uri>[]; |
| for (final Object? item in items) { |
| if (item is! String || item == '') { |
| _logger.printError('$singularName manifest contains a null or empty uri.'); |
| continue; |
| } |
| try { |
| results.add(Uri(pathSegments: item.split('/'))); |
| } on FormatException { |
| _logger.printError('$singularName manifest contains invalid uri: $item.'); |
| } |
| } |
| return results; |
| } |
| |
| /// Whether a synthetic flutter_gen package should be generated. |
| /// |
| /// This can be provided to the [Pub] interface to inject a new entry |
| /// into the package_config.json file which points to `.dart_tool/flutter_gen`. |
| /// |
| /// This allows generated source code to be imported using a package |
| /// alias. |
| late final bool generateSyntheticPackage = _computeGenerateSyntheticPackage(); |
| bool _computeGenerateSyntheticPackage() { |
| if (!_flutterDescriptor.containsKey('generate')) { |
| return false; |
| } |
| final Object? value = _flutterDescriptor['generate']; |
| if (value is! bool) { |
| return false; |
| } |
| return value; |
| } |
| } |
| |
| class Font { |
| Font(this.familyName, this.fontAssets) |
| : assert(fontAssets.isNotEmpty); |
| |
| final String familyName; |
| final List<FontAsset> fontAssets; |
| |
| Map<String, Object?> get descriptor { |
| return <String, Object?>{ |
| 'family': familyName, |
| 'fonts': fontAssets.map<Map<String, Object?>>((FontAsset a) => a.descriptor).toList(), |
| }; |
| } |
| |
| @override |
| String toString() => '$runtimeType(family: $familyName, assets: $fontAssets)'; |
| } |
| |
| class FontAsset { |
| FontAsset(this.assetUri, {this.weight, this.style}); |
| |
| final Uri assetUri; |
| final int? weight; |
| final String? style; |
| |
| Map<String, Object?> get descriptor { |
| final Map<String, Object?> descriptor = <String, Object?>{}; |
| if (weight != null) { |
| descriptor['weight'] = weight; |
| } |
| |
| if (style != null) { |
| descriptor['style'] = style; |
| } |
| |
| descriptor['asset'] = assetUri.path; |
| return descriptor; |
| } |
| |
| @override |
| String toString() => '$runtimeType(asset: ${assetUri.path}, weight; $weight, style: $style)'; |
| } |
| |
| |
| bool _validate(Object? manifest, Logger logger) { |
| final List<String> errors = <String>[]; |
| if (manifest is! YamlMap) { |
| errors.add('Expected YAML map'); |
| } else { |
| for (final MapEntry<Object?, Object?> kvp in manifest.entries) { |
| if (kvp.key is! String) { |
| errors.add('Expected YAML key to be a string, but got ${kvp.key}.'); |
| continue; |
| } |
| switch (kvp.key as String?) { |
| case 'name': |
| if (kvp.value is! String) { |
| errors.add('Expected "${kvp.key}" to be a string, but got ${kvp.value}.'); |
| } |
| case 'flutter': |
| if (kvp.value == null) { |
| continue; |
| } |
| if (kvp.value is! YamlMap) { |
| errors.add('Expected "${kvp.key}" section to be an object or null, but got ${kvp.value}.'); |
| } else { |
| _validateFlutter(kvp.value as YamlMap?, errors); |
| } |
| default: |
| // additionalProperties are allowed. |
| break; |
| } |
| } |
| } |
| |
| if (errors.isNotEmpty) { |
| logger.printStatus('Error detected in pubspec.yaml:', emphasis: true); |
| logger.printError(errors.join('\n')); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void _validateFlutter(YamlMap? yaml, List<String> errors) { |
| if (yaml == null) { |
| return; |
| } |
| for (final MapEntry<Object?, Object?> kvp in yaml.entries) { |
| final Object? yamlKey = kvp.key; |
| final Object? yamlValue = kvp.value; |
| if (yamlKey is! String) { |
| errors.add('Expected YAML key to be a string, but got $yamlKey (${yamlValue.runtimeType}).'); |
| continue; |
| } |
| switch (yamlKey) { |
| case 'uses-material-design': |
| if (yamlValue is! bool) { |
| errors.add('Expected "$yamlKey" to be a bool, but got $yamlValue (${yamlValue.runtimeType}).'); |
| } |
| case 'assets': |
| errors.addAll(_validateAssets(yamlValue)); |
| case 'shaders': |
| if (yamlValue is! YamlList) { |
| errors.add('Expected "$yamlKey" to be a list, but got $yamlValue (${yamlValue.runtimeType}).'); |
| } else if (yamlValue.isEmpty) { |
| break; |
| } else if (yamlValue[0] is! String) { |
| errors.add( |
| 'Expected "$yamlKey" to be a list of strings, but the first element is $yamlValue (${yamlValue.runtimeType}).', |
| ); |
| } |
| case 'models': |
| if (yamlValue is! YamlList) { |
| errors.add('Expected "$yamlKey" to be a list, but got $yamlValue (${yamlValue.runtimeType}).'); |
| } else if (yamlValue.isEmpty) { |
| break; |
| } else if (yamlValue[0] is! String) { |
| errors.add( |
| 'Expected "$yamlKey" to be a list of strings, but the first element is $yamlValue (${yamlValue.runtimeType}).', |
| ); |
| } |
| case 'fonts': |
| if (yamlValue is! YamlList) { |
| errors.add('Expected "$yamlKey" to be a list, but got $yamlValue (${yamlValue.runtimeType}).'); |
| } else if (yamlValue.isEmpty) { |
| break; |
| } else if (yamlValue.first is! YamlMap) { |
| errors.add( |
| 'Expected "$yamlKey" to contain maps, but the first element is $yamlValue (${yamlValue.runtimeType}).', |
| ); |
| } else { |
| _validateFonts(yamlValue, errors); |
| } |
| case 'licenses': |
| final (_, List<String> filesErrors) = _parseList<String>(yamlValue, '"$yamlKey"', 'files'); |
| errors.addAll(filesErrors); |
| case 'module': |
| if (yamlValue is! YamlMap) { |
| errors.add('Expected "$yamlKey" to be an object, but got $yamlValue (${yamlValue.runtimeType}).'); |
| break; |
| } |
| |
| if (yamlValue['androidX'] != null && yamlValue['androidX'] is! bool) { |
| errors.add('The "androidX" value must be a bool if set.'); |
| } |
| if (yamlValue['androidPackage'] != null && yamlValue['androidPackage'] is! String) { |
| errors.add('The "androidPackage" value must be a string if set.'); |
| } |
| if (yamlValue['iosBundleIdentifier'] != null && yamlValue['iosBundleIdentifier'] is! String) { |
| errors.add('The "iosBundleIdentifier" section must be a string if set.'); |
| } |
| case 'plugin': |
| if (yamlValue is! YamlMap) { |
| errors.add('Expected "$yamlKey" to be an object, but got $yamlValue (${yamlValue.runtimeType}).'); |
| break; |
| } |
| final List<String> pluginErrors = Plugin.validatePluginYaml(yamlValue); |
| errors.addAll(pluginErrors); |
| case 'generate': |
| break; |
| case 'deferred-components': |
| _validateDeferredComponents(kvp, errors); |
| case 'disable-swift-package-manager': |
| if (yamlValue is! bool) { |
| errors.add('Expected "$yamlKey" to be a bool, but got $yamlValue (${yamlValue.runtimeType}).'); |
| } |
| default: |
| errors.add('Unexpected child "$yamlKey" found under "flutter".'); |
| break; |
| } |
| } |
| } |
| |
| (List<T>? result, List<String> errors) _parseList<T>(Object? yamlList, String context, String typeAlias) { |
| final List<String> errors = <String>[]; |
| |
| if (yamlList is! YamlList) { |
| final String message = 'Expected $context to be a list of $typeAlias, but got $yamlList (${yamlList.runtimeType}).'; |
| return (null, <String>[message]); |
| } |
| |
| for (int i = 0; i < yamlList.length; i++) { |
| if (yamlList[i] is! T) { |
| // ignore: avoid_dynamic_calls |
| errors.add('Expected $context to be a list of $typeAlias, but element at index $i was a ${yamlList[i].runtimeType}.'); |
| } |
| } |
| |
| return errors.isEmpty ? (List<T>.from(yamlList), errors) : (null, errors); |
| } |
| |
| void _validateDeferredComponents(MapEntry<Object?, Object?> kvp, List<String> errors) { |
| final Object? yamlList = kvp.value; |
| if (yamlList != null && (yamlList is! YamlList || yamlList[0] is! YamlMap)) { |
| errors.add('Expected "${kvp.key}" to be a list, but got $yamlList (${yamlList.runtimeType}).'); |
| } else if (yamlList is YamlList) { |
| for (int i = 0; i < yamlList.length; i++) { |
| final Object? valueMap = yamlList[i]; |
| if (valueMap is! YamlMap) { |
| // ignore: avoid_dynamic_calls |
| errors.add('Expected the $i element in "${kvp.key}" to be a map, but got ${yamlList[i]} (${yamlList[i].runtimeType}).'); |
| continue; |
| } |
| if (!valueMap.containsKey('name') || valueMap['name'] is! String) { |
| errors.add('Expected the $i element in "${kvp.key}" to have required key "name" of type String'); |
| } |
| if (valueMap.containsKey('libraries')) { |
| final (_, List<String> librariesErrors) = _parseList<String>( |
| valueMap['libraries'], |
| '"libraries" key in the element at index $i of "${kvp.key}"', |
| 'String', |
| ); |
| errors.addAll(librariesErrors); |
| } |
| if (valueMap.containsKey('assets')) { |
| errors.addAll(_validateAssets(valueMap['assets'])); |
| } |
| } |
| } |
| } |
| |
| List<String> _validateAssets(Object? yaml) { |
| final (_, List<String> errors) = _computeAssetsSafe(yaml); |
| return errors; |
| } |
| |
| // TODO(andrewkolos): We end up parsing the assets section twice, once during |
| // validation and once when the assets getter is called. We should consider |
| // refactoring this class to parse and store everything in the constructor. |
| // https://github.com/flutter/flutter/issues/139183 |
| (List<AssetsEntry>, List<String> errors) _computeAssetsSafe(Object? yaml) { |
| if (yaml == null) { |
| return (const <AssetsEntry>[], const <String>[]); |
| } |
| if (yaml is! YamlList) { |
| final String error = 'Expected "assets" to be a list, but got $yaml (${yaml.runtimeType}).'; |
| return (const <AssetsEntry>[], <String>[error]); |
| } |
| final List<AssetsEntry> results = <AssetsEntry>[]; |
| final List<String> errors = <String>[]; |
| for (final Object? rawAssetEntry in yaml) { |
| final (AssetsEntry? parsed, String? error) = AssetsEntry.parseFromYamlSafe(rawAssetEntry); |
| if (parsed != null) { |
| results.add(parsed); |
| } |
| if (error != null) { |
| errors.add(error); |
| } |
| } |
| return (results, errors); |
| } |
| |
| List<AssetsEntry> _computeAssets(Object? assetsSection) { |
| final (List<AssetsEntry> result, List<String> errors) = _computeAssetsSafe(assetsSection); |
| if (errors.isNotEmpty) { |
| throw Exception('Uncaught error(s) in assets section: ' |
| '${errors.join('\n')}'); |
| } |
| return result; |
| } |
| |
| void _validateFonts(YamlList fonts, List<String> errors) { |
| const Set<int> fontWeights = <int>{ |
| 100, 200, 300, 400, 500, 600, 700, 800, 900, |
| }; |
| for (final Object? fontMap in fonts) { |
| if (fontMap is! YamlMap) { |
| errors.add('Unexpected child "$fontMap" found under "fonts". Expected a map.'); |
| continue; |
| } |
| for (final Object? key in fontMap.keys.where((Object? key) => key != 'family' && key != 'fonts')) { |
| errors.add('Unexpected child "$key" found under "fonts".'); |
| } |
| if (fontMap['family'] != null && fontMap['family'] is! String) { |
| errors.add('Font family must either be null or a String.'); |
| } |
| if (fontMap['fonts'] == null) { |
| continue; |
| } else if (fontMap['fonts'] is! YamlList) { |
| errors.add('Expected "fonts" to either be null or a list.'); |
| continue; |
| } |
| for (final Object? fontMapList in fontMap['fonts'] as List<Object?>) { |
| if (fontMapList is! YamlMap) { |
| errors.add('Expected "fonts" to be a list of maps.'); |
| continue; |
| } |
| for (final MapEntry<Object?, Object?> kvp in fontMapList.entries) { |
| final Object? fontKey = kvp.key; |
| if (fontKey is! String) { |
| errors.add('Expected "$fontKey" under "fonts" to be a string.'); |
| } |
| switch (fontKey) { |
| case 'asset': |
| if (kvp.value is! String) { |
| errors.add('Expected font asset ${kvp.value} ((${kvp.value.runtimeType})) to be a string.'); |
| } |
| case 'weight': |
| if (!fontWeights.contains(kvp.value)) { |
| errors.add('Invalid value ${kvp.value} ((${kvp.value.runtimeType})) for font -> weight.'); |
| } |
| case 'style': |
| if (kvp.value != 'normal' && kvp.value != 'italic') { |
| errors.add('Invalid value ${kvp.value} ((${kvp.value.runtimeType})) for font -> style.'); |
| } |
| default: |
| errors.add('Unexpected key $fontKey ((${kvp.value.runtimeType})) under font.'); |
| break; |
| } |
| } |
| } |
| } |
| } |
| |
| /// Represents an entry under the `assets` section of a pubspec. |
| @immutable |
| class AssetsEntry { |
| const AssetsEntry({ |
| required this.uri, |
| this.flavors = const <String>{}, |
| this.transformers = const <AssetTransformerEntry>[], |
| }); |
| |
| final Uri uri; |
| final Set<String> flavors; |
| final List<AssetTransformerEntry> transformers; |
| |
| static const String _pathKey = 'path'; |
| static const String _flavorKey = 'flavors'; |
| static const String _transformersKey = 'transformers'; |
| |
| static AssetsEntry? parseFromYaml(Object? yaml) { |
| final (AssetsEntry? value, String? error) = parseFromYamlSafe(yaml); |
| if (error != null) { |
| throw Exception('Unexpected error when parsing assets entry'); |
| } |
| return value!; |
| } |
| |
| static (AssetsEntry? assetsEntry, String? error) parseFromYamlSafe(Object? yaml) { |
| |
| (Uri?, String?) tryParseUri(String uri) { |
| try { |
| return (Uri(pathSegments: uri.split('/')), null); |
| } on FormatException { |
| return (null, 'Asset manifest contains invalid uri: $uri.'); |
| } |
| } |
| |
| if (yaml == null || yaml == '') { |
| return (null, 'Asset manifest contains a null or empty uri.'); |
| } |
| |
| if (yaml is String) { |
| final (Uri? uri, String? error) = tryParseUri(yaml); |
| return uri == null ? (null, error) : (AssetsEntry(uri: uri), null); |
| } |
| |
| if (yaml is Map) { |
| if (yaml.keys.isEmpty) { |
| return (null, null); |
| } |
| |
| final Object? path = yaml[_pathKey]; |
| |
| if (path == null || path is! String) { |
| return (null, 'Asset manifest entry is malformed. ' |
| 'Expected asset entry to be either a string or a map ' |
| 'containing a "$_pathKey" entry. Got ${path.runtimeType} instead.'); |
| } |
| |
| final (List<String>? flavors, List<String> flavorsErrors) = _parseFlavorsSection(yaml[_flavorKey]); |
| final (List<AssetTransformerEntry>? transformers, List<String> transformersErrors) = _parseTransformersSection(yaml[_transformersKey]); |
| |
| final List<String> errors = <String>[ |
| ...flavorsErrors.map((String e) => 'In $_flavorKey section of asset "$path": $e'), |
| ...transformersErrors.map((String e) => 'In $_transformersKey section of asset "$path": $e'), |
| ]; |
| if (errors.isNotEmpty) { |
| return ( |
| null, |
| <String>[ |
| 'Unable to parse assets section.', |
| ...errors |
| ].join('\n'), |
| ); |
| } |
| |
| return ( |
| AssetsEntry( |
| uri: Uri(pathSegments: path.split('/')), |
| flavors: Set<String>.from(flavors ?? <String>[]), |
| transformers: transformers ?? <AssetTransformerEntry>[], |
| ), |
| null, |
| ); |
| } |
| |
| return (null, 'Assets entry had unexpected shape. ' |
| 'Expected a string or an object. Got ${yaml.runtimeType} instead.'); |
| } |
| |
| static (List<String>? flavors, List<String> errors) _parseFlavorsSection(Object? yaml) { |
| if (yaml == null) { |
| return (null, <String>[]); |
| } |
| |
| return _parseList<String>(yaml, _flavorKey, 'String'); |
| } |
| |
| static (List<AssetTransformerEntry>?, List<String> errors) _parseTransformersSection(Object? yaml) { |
| if (yaml == null) { |
| return (null, <String>[]); |
| } |
| final (List<YamlMap>? yamlObjects, List<String> listErrors) = _parseList<YamlMap>( |
| yaml, |
| '$_transformersKey list', |
| 'Map', |
| ); |
| |
| if (listErrors.isNotEmpty) { |
| return (null, listErrors); |
| } |
| |
| final List<AssetTransformerEntry> transformers = <AssetTransformerEntry>[]; |
| final List<String> errors = <String>[]; |
| for (final YamlMap yaml in yamlObjects!) { |
| final (AssetTransformerEntry? transformerEntry, List<String> transformerErrors) = AssetTransformerEntry.tryParse(yaml); |
| if (transformerEntry != null) { |
| transformers.add(transformerEntry); |
| } else { |
| errors.addAll(transformerErrors); |
| } |
| } |
| |
| if (errors.isEmpty) { |
| return (transformers, errors); |
| } |
| return (null, errors); |
| } |
| |
| @override |
| bool operator ==(Object other) { |
| if (other is! AssetsEntry) { |
| return false; |
| } |
| |
| return uri == other.uri && setEquals(flavors, other.flavors); |
| } |
| |
| @override |
| int get hashCode => Object.hashAll(<Object?>[ |
| uri.hashCode, |
| Object.hashAllUnordered(flavors), |
| Object.hashAll(transformers), |
| ]); |
| |
| @override |
| String toString() => 'AssetsEntry(uri: $uri, flavors: $flavors, transformers: $transformers)'; |
| } |
| |
| |
| /// Represents an entry in the "transformers" section of an asset. |
| @immutable |
| final class AssetTransformerEntry { |
| const AssetTransformerEntry({ |
| required this.package, |
| required List<String>? args, |
| }): args = args ?? const <String>[]; |
| |
| final String package; |
| final List<String>? args; |
| |
| static (AssetTransformerEntry? entry, List<String> errors) tryParse(Object? yaml) { |
| if (yaml == null) { |
| return (null, <String>['Transformer entry is null.']); |
| } |
| if (yaml is! YamlMap) { |
| return (null, <String>['Expected entry to be a map. Found ${yaml.runtimeType} instead']); |
| } |
| |
| final Object? package = yaml['package']; |
| if (package is! String || package.isEmpty) { |
| return (null, <String>['Expected "package" to be a String. Found ${package.runtimeType} instead.']); |
| } |
| |
| final (List<String>? args, List<String> argsErrors) = _parseArgsSection(yaml['args']); |
| if (argsErrors.isNotEmpty) { |
| return (null, argsErrors.map((String e) => 'In args section of transformer using package "$package": $e').toList()); |
| } |
| |
| return ( |
| AssetTransformerEntry( |
| package: package, |
| args: args, |
| ), |
| <String>[], |
| ); |
| } |
| |
| static (List<String>? args, List<String> errors) _parseArgsSection(Object? yaml) { |
| if (yaml == null) { |
| return (null, <String>[]); |
| } |
| return _parseList(yaml, 'args', 'String'); |
| } |
| |
| @override |
| bool operator ==(Object other) { |
| if (identical(this, other)) { |
| return true; |
| } |
| if (other is! AssetTransformerEntry) { |
| return false; |
| } |
| |
| final bool argsAreEqual = (() { |
| if (args == null && other.args == null) { |
| return true; |
| } |
| if (args?.length != other.args?.length) { |
| return false; |
| } |
| |
| for (int index = 0; index < args!.length; index += 1) { |
| if (args![index] != other.args![index]) { |
| return false; |
| } |
| } |
| return true; |
| })(); |
| |
| return package == other.package && argsAreEqual; |
| } |
| |
| @override |
| int get hashCode => Object.hashAll( |
| <Object?>[ |
| package.hashCode, |
| args?.map((String e) => e.hashCode), |
| ], |
| ); |
| |
| @override |
| String toString() { |
| return 'AssetTransformerEntry(package: $package, args: $args)'; |
| } |
| } |