add parsing of assets transformer declarations in pubspec.yaml (#143557)
In service of https://github.com/flutter/flutter/issues/143348. This PR enables parsing of the pubspec yaml schemes for assets with transformations as described in #143348.
This commit is contained in:
parent
848aa5080b
commit
3a18473bd6
@ -519,7 +519,8 @@ void _validateFlutter(YamlMap? yaml, List<String> errors) {
|
|||||||
_validateFonts(yamlValue, errors);
|
_validateFonts(yamlValue, errors);
|
||||||
}
|
}
|
||||||
case 'licenses':
|
case 'licenses':
|
||||||
errors.addAll(_validateList<String>(yamlValue, '"$yamlKey"', 'files'));
|
final (_, List<String> filesErrors) = _parseList<String>(yamlValue, '"$yamlKey"', 'files');
|
||||||
|
errors.addAll(filesErrors);
|
||||||
case 'module':
|
case 'module':
|
||||||
if (yamlValue is! YamlMap) {
|
if (yamlValue is! YamlMap) {
|
||||||
errors.add('Expected "$yamlKey" to be an object, but got $yamlValue (${yamlValue.runtimeType}).');
|
errors.add('Expected "$yamlKey" to be an object, but got $yamlValue (${yamlValue.runtimeType}).');
|
||||||
@ -553,11 +554,12 @@ void _validateFlutter(YamlMap? yaml, List<String> errors) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> _validateList<T>(Object? yamlList, String context, String typeAlias) {
|
(List<T>? result, List<String> errors) _parseList<T>(Object? yamlList, String context, String typeAlias) {
|
||||||
final List<String> errors = <String>[];
|
final List<String> errors = <String>[];
|
||||||
|
|
||||||
if (yamlList is! YamlList) {
|
if (yamlList is! YamlList) {
|
||||||
return <String>['Expected $context to be a list of $typeAlias, but got $yamlList (${yamlList.runtimeType}).'];
|
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++) {
|
for (int i = 0; i < yamlList.length; i++) {
|
||||||
@ -567,8 +569,9 @@ List<String> _validateList<T>(Object? yamlList, String context, String typeAlias
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors;
|
return errors.isEmpty ? (List<T>.from(yamlList), errors) : (null, errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _validateDeferredComponents(MapEntry<Object?, Object?> kvp, List<String> errors) {
|
void _validateDeferredComponents(MapEntry<Object?, Object?> kvp, List<String> errors) {
|
||||||
final Object? yamlList = kvp.value;
|
final Object? yamlList = kvp.value;
|
||||||
if (yamlList != null && (yamlList is! YamlList || yamlList[0] is! YamlMap)) {
|
if (yamlList != null && (yamlList is! YamlList || yamlList[0] is! YamlMap)) {
|
||||||
@ -585,11 +588,12 @@ void _validateDeferredComponents(MapEntry<Object?, Object?> kvp, List<String> er
|
|||||||
errors.add('Expected the $i element in "${kvp.key}" to have required key "name" of type String');
|
errors.add('Expected the $i element in "${kvp.key}" to have required key "name" of type String');
|
||||||
}
|
}
|
||||||
if (valueMap.containsKey('libraries')) {
|
if (valueMap.containsKey('libraries')) {
|
||||||
errors.addAll(_validateList<String>(
|
final (_, List<String> librariesErrors) = _parseList<String>(
|
||||||
valueMap['libraries'],
|
valueMap['libraries'],
|
||||||
'"libraries" key in the element at index $i of "${kvp.key}"',
|
'"libraries" key in the element at index $i of "${kvp.key}"',
|
||||||
'String',
|
'String',
|
||||||
));
|
);
|
||||||
|
errors.addAll(librariesErrors);
|
||||||
}
|
}
|
||||||
if (valueMap.containsKey('assets')) {
|
if (valueMap.containsKey('assets')) {
|
||||||
errors.addAll(_validateAssets(valueMap['assets']));
|
errors.addAll(_validateAssets(valueMap['assets']));
|
||||||
@ -697,13 +701,16 @@ class AssetsEntry {
|
|||||||
const AssetsEntry({
|
const AssetsEntry({
|
||||||
required this.uri,
|
required this.uri,
|
||||||
this.flavors = const <String>{},
|
this.flavors = const <String>{},
|
||||||
|
this.transformers = const <AssetTransformerEntry>[],
|
||||||
});
|
});
|
||||||
|
|
||||||
final Uri uri;
|
final Uri uri;
|
||||||
final Set<String> flavors;
|
final Set<String> flavors;
|
||||||
|
final List<AssetTransformerEntry> transformers;
|
||||||
|
|
||||||
static const String _pathKey = 'path';
|
static const String _pathKey = 'path';
|
||||||
static const String _flavorKey = 'flavors';
|
static const String _flavorKey = 'flavors';
|
||||||
|
static const String _transformersKey = 'transformers';
|
||||||
|
|
||||||
static AssetsEntry? parseFromYaml(Object? yaml) {
|
static AssetsEntry? parseFromYaml(Object? yaml) {
|
||||||
final (AssetsEntry? value, String? error) = parseFromYamlSafe(yaml);
|
final (AssetsEntry? value, String? error) = parseFromYamlSafe(yaml);
|
||||||
@ -738,7 +745,6 @@ class AssetsEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final Object? path = yaml[_pathKey];
|
final Object? path = yaml[_pathKey];
|
||||||
final Object? flavors = yaml[_flavorKey];
|
|
||||||
|
|
||||||
if (path == null || path is! String) {
|
if (path == null || path is! String) {
|
||||||
return (null, 'Asset manifest entry is malformed. '
|
return (null, 'Asset manifest entry is malformed. '
|
||||||
@ -746,41 +752,76 @@ class AssetsEntry {
|
|||||||
'containing a "$_pathKey" entry. Got ${path.runtimeType} instead.');
|
'containing a "$_pathKey" entry. Got ${path.runtimeType} instead.');
|
||||||
}
|
}
|
||||||
|
|
||||||
final Uri uri = Uri(pathSegments: path.split('/'));
|
final (List<String>? flavors, List<String> flavorsErrors) = _parseFlavorsSection(yaml[_flavorKey]);
|
||||||
|
final (List<AssetTransformerEntry>? transformers, List<String> transformersErrors) = _parseTransformersSection(yaml[_transformersKey]);
|
||||||
|
|
||||||
if (flavors == null) {
|
final List<String> errors = <String>[
|
||||||
return (AssetsEntry(uri: uri), null);
|
...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'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flavors is! YamlList) {
|
return (
|
||||||
return(null, 'Asset manifest entry is malformed. '
|
AssetsEntry(
|
||||||
'Expected "$_flavorKey" entry to be a list of strings. '
|
uri: Uri(pathSegments: path.split('/')),
|
||||||
'Got ${flavors.runtimeType} instead.');
|
flavors: Set<String>.from(flavors ?? <String>[]),
|
||||||
}
|
transformers: transformers ?? <AssetTransformerEntry>[],
|
||||||
|
),
|
||||||
final List<String> flavorsListErrors = _validateList<String>(
|
null,
|
||||||
flavors,
|
|
||||||
'flavors list of entry "$path"',
|
|
||||||
'String',
|
|
||||||
);
|
);
|
||||||
if (flavorsListErrors.isNotEmpty) {
|
|
||||||
return (null, 'Asset manifest entry is malformed. '
|
|
||||||
'Expected "$_flavorKey" entry to be a list of strings.\n'
|
|
||||||
'${flavorsListErrors.join('\n')}');
|
|
||||||
}
|
|
||||||
|
|
||||||
final AssetsEntry entry = AssetsEntry(
|
|
||||||
uri: Uri(pathSegments: path.split('/')),
|
|
||||||
flavors: Set<String>.from(flavors),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (entry, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (null, 'Assets entry had unexpected shape. '
|
return (null, 'Assets entry had unexpected shape. '
|
||||||
'Expected a string or an object. Got ${yaml.runtimeType} instead.');
|
'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
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (other is! AssetsEntry) {
|
if (other is! AssetsEntry) {
|
||||||
@ -799,3 +840,91 @@ class AssetsEntry {
|
|||||||
@override
|
@override
|
||||||
String toString() => 'AssetsEntry(uri: $uri, flavors: $flavors)';
|
String toString() => 'AssetsEntry(uri: $uri, flavors: $flavors)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// 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)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -154,8 +154,9 @@ flutter:
|
|||||||
''';
|
''';
|
||||||
FlutterManifest.createFromString(manifest, logger: logger);
|
FlutterManifest.createFromString(manifest, logger: logger);
|
||||||
expect(logger.errorText, contains(
|
expect(logger.errorText, contains(
|
||||||
'Asset manifest entry is malformed. '
|
'Unable to parse assets section.\n'
|
||||||
'Expected "flavors" entry to be a list of strings.',
|
'In flavors section of asset "assets/vanilla/": Expected flavors '
|
||||||
|
'to be a list of String, but element at index 0 was a YamlMap.\n'
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,166 @@
|
|||||||
|
// 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:flutter_tools/src/base/logger.dart';
|
||||||
|
import 'package:flutter_tools/src/flutter_manifest.dart';
|
||||||
|
|
||||||
|
import '../src/common.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('parsing of assets section in flutter manifests with asset transformers', () {
|
||||||
|
testWithoutContext('parses an asset with a simple transformation', () async {
|
||||||
|
final BufferLogger logger = BufferLogger.test();
|
||||||
|
const String manifest = '''
|
||||||
|
name: test
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
flutter:
|
||||||
|
uses-material-design: true
|
||||||
|
assets:
|
||||||
|
- path: asset/hello.txt
|
||||||
|
transformers:
|
||||||
|
- package: my_package
|
||||||
|
''';
|
||||||
|
final FlutterManifest? parsedManifest = FlutterManifest.createFromString(manifest, logger: logger);
|
||||||
|
|
||||||
|
expect(parsedManifest!.assets, <AssetsEntry>[
|
||||||
|
AssetsEntry(
|
||||||
|
uri: Uri.parse('asset/hello.txt'),
|
||||||
|
transformers: const <AssetTransformerEntry>[
|
||||||
|
AssetTransformerEntry(package: 'my_package', args: <String>[])
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(logger.errorText, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('parses an asset with a transformation that has args', () async {
|
||||||
|
final BufferLogger logger = BufferLogger.test();
|
||||||
|
const String manifest = '''
|
||||||
|
name: test
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
flutter:
|
||||||
|
uses-material-design: true
|
||||||
|
assets:
|
||||||
|
- path: asset/hello.txt
|
||||||
|
transformers:
|
||||||
|
- package: my_package
|
||||||
|
args: ["-e", "--color", "purple"]
|
||||||
|
''';
|
||||||
|
final FlutterManifest? parsedManifest = FlutterManifest.createFromString(manifest, logger: logger);
|
||||||
|
|
||||||
|
expect(parsedManifest!.assets, <AssetsEntry>[
|
||||||
|
AssetsEntry(
|
||||||
|
uri: Uri.parse('asset/hello.txt'),
|
||||||
|
transformers: const <AssetTransformerEntry>[
|
||||||
|
AssetTransformerEntry(
|
||||||
|
package: 'my_package',
|
||||||
|
args: <String>['-e', '--color', 'purple'],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
expect(logger.errorText, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('fails when a transformers section is not a list', () async {
|
||||||
|
final BufferLogger logger = BufferLogger.test();
|
||||||
|
const String manifest = '''
|
||||||
|
name: test
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
flutter:
|
||||||
|
uses-material-design: true
|
||||||
|
assets:
|
||||||
|
- path: asset/hello.txt
|
||||||
|
transformers:
|
||||||
|
- my_transformer
|
||||||
|
''';
|
||||||
|
FlutterManifest.createFromString(manifest, logger: logger);
|
||||||
|
expect(
|
||||||
|
logger.errorText,
|
||||||
|
'Unable to parse assets section.\n'
|
||||||
|
'In transformers section of asset "asset/hello.txt": Expected '
|
||||||
|
'transformers list to be a list of Map, but element at index 0 was a String.\n',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
testWithoutContext('fails when a transformers section package is not a string', () async {
|
||||||
|
final BufferLogger logger = BufferLogger.test();
|
||||||
|
|
||||||
|
const String manifest = '''
|
||||||
|
name: test
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
flutter:
|
||||||
|
uses-material-design: true
|
||||||
|
assets:
|
||||||
|
- path: asset/hello.txt
|
||||||
|
transformers:
|
||||||
|
- package:
|
||||||
|
i am a key: i am a value
|
||||||
|
''';
|
||||||
|
FlutterManifest.createFromString(manifest, logger: logger);
|
||||||
|
expect(
|
||||||
|
logger.errorText,
|
||||||
|
'Unable to parse assets section.\n'
|
||||||
|
'In transformers section of asset "asset/hello.txt": '
|
||||||
|
'Expected "package" to be a String. Found YamlMap instead.\n',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('fails when a transformer is missing the package field', () async {
|
||||||
|
final BufferLogger logger = BufferLogger.test();
|
||||||
|
const String manifest = '''
|
||||||
|
name: test
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
flutter:
|
||||||
|
uses-material-design: true
|
||||||
|
assets:
|
||||||
|
- path: asset/hello.txt
|
||||||
|
transformers:
|
||||||
|
- args: ["-e"]
|
||||||
|
''';
|
||||||
|
FlutterManifest.createFromString(manifest, logger: logger);
|
||||||
|
expect(
|
||||||
|
logger.errorText,
|
||||||
|
'Unable to parse assets section.\n'
|
||||||
|
'In transformers section of asset "asset/hello.txt": Expected "package" to be a '
|
||||||
|
'String. Found Null instead.\n',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('fails when a transformer has args field that is not a list of strings', () async {
|
||||||
|
final BufferLogger logger = BufferLogger.test();
|
||||||
|
const String manifest = '''
|
||||||
|
name: test
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
flutter:
|
||||||
|
uses-material-design: true
|
||||||
|
assets:
|
||||||
|
- path: asset/hello.txt
|
||||||
|
transformers:
|
||||||
|
- package: my_transformer
|
||||||
|
args: hello
|
||||||
|
''';
|
||||||
|
FlutterManifest.createFromString(manifest, logger: logger);
|
||||||
|
expect(
|
||||||
|
logger.errorText,
|
||||||
|
'Unable to parse assets section.\n'
|
||||||
|
'In transformers section of asset "asset/hello.txt": In args section '
|
||||||
|
'of transformer using package "my_transformer": Expected args to be a '
|
||||||
|
'list of String, but got hello (String).\n',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user