blob: f5ef0b02dcc66d94d826abe76f87ad5117ab27bd [file] [log] [blame]
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:convert';
import 'package:path/path.dart' as p;
import 'package:pub_semver/pub_semver.dart';
import 'language_version.dart';
/// Contents of a `.dart_tool/package_config.json` file.
class PackageConfig {
/// Version of the configuration in the `.dart_tool/package_config.json` file.
///
/// The only supported value as of writing is `2`.
int configVersion;
/// Packages configured.
List<PackageConfigEntry> packages;
/// Date-time the `.dart_tool/package_config.json` file was generated.
///
/// `null` if not given.
DateTime? generated;
/// Tool that generated the `.dart_tool/package_config.json` file.
///
/// For `pub` this is always `'pub'`.
///
/// `null` if not given.
String? generator;
/// Version of the tool that generated the `.dart_tool/package_config.json`
/// file.
///
/// For `pub` this is the Dart SDK version from which `pub get` was called.
///
/// `null` if not given.
Version? generatorVersion;
/// Additional properties not in the specification for the
/// `.dart_tool/package_config.json` file.
Map<String, dynamic> additionalProperties;
PackageConfig({
required this.configVersion,
required this.packages,
this.generated,
this.generator,
this.generatorVersion,
Map<String, dynamic>? additionalProperties,
}) : additionalProperties = additionalProperties ?? {};
/// Create [PackageConfig] from JSON [data].
///
/// Throws [FormatException], if format is invalid, this does not validate the
/// contents only that the format is correct.
factory PackageConfig.fromJson(Object data) {
if (data is! Map<String, dynamic>) {
throw FormatException('package_config.json must be a JSON object');
}
final root = data;
void throwFormatException(String property, String mustBe) =>
throw FormatException(
'"$property" in .dart_tool/package_config.json $mustBe',
);
/// Read the 'configVersion' property
final configVersion = root['configVersion'];
if (configVersion is! int) {
throwFormatException('configVersion', 'must be an integer');
}
if (configVersion != 2) {
throwFormatException(
'configVersion',
'must be 2 (the only supported version)',
);
}
final packagesRaw = root['packages'];
if (packagesRaw is! List) {
throwFormatException('packages', 'must be a list');
}
final packages = <PackageConfigEntry>[];
for (final entry in packagesRaw) {
packages.add(PackageConfigEntry.fromJson(entry));
}
// Read the 'generated' property
DateTime? generated;
final generatedRaw = root['generated'];
if (generatedRaw != null) {
if (generatedRaw is! String) {
throwFormatException('generated', 'must be a string, if given');
}
generated = DateTime.parse(generatedRaw);
}
// Read the 'generator' property
final generator = root['generator'];
if (generator != null && generator is! String) {
throw FormatException(
'"generator" in package_config.json must be a string, if given',
);
}
// Read the 'generatorVersion' property
Version? generatorVersion;
final generatorVersionRaw = root['generatorVersion'];
if (generatorVersionRaw != null) {
if (generatorVersionRaw is! String) {
throwFormatException('generatorVersion', 'must be a string, if given');
}
try {
generatorVersion = Version.parse(generatorVersionRaw);
} on FormatException catch (e) {
throwFormatException(
'generatorVersion',
'must be a semver version, if given, error: ${e.message}',
);
}
}
return PackageConfig(
configVersion: configVersion as int,
packages: packages,
generated: generated,
generator: generator,
generatorVersion: generatorVersion,
additionalProperties: Map.fromEntries(
root.entries.where(
(e) => !{
'configVersion',
'packages',
'generated',
'generator',
'generatorVersion',
}.contains(e.key),
),
),
);
}
/// Convert to JSON structure.
Map<String, Object?> toJson() => {
'configVersion': configVersion,
'packages': packages.map((p) => p.toJson()).toList(),
'generated': generated?.toUtc().toIso8601String(),
'generator': generator,
'generatorVersion': generatorVersion?.toString(),
}..addAll(additionalProperties);
// We allow the package called 'flutter_gen' to be injected into
// package_config.
//
// This is somewhat a hack. But it allows flutter to generate code in a
// package as it likes.
//
// See https://github.com/flutter/flutter/issues/73870 .
Iterable<PackageConfigEntry> get nonInjectedPackages =>
packages.where((package) => package.name != 'flutter_gen');
}
class PackageConfigEntry {
/// Package name.
String name;
/// Root [Uri] of the package.
///
/// This specifies the root folder of the package, all files below this folder
/// is considered part of this package.
Uri rootUri;
/// Relative URI path of the library folder relative to [rootUri].
///
/// Import statements in Dart programs are resolved relative to this folder.
/// This must be in the sub-tree under [rootUri].
///
/// `null` if not given.
Uri? packageUri;
/// Language version used by package.
///
/// Given as `<major>.<minor>` version, similar to the `// @dart = X.Y`
/// comment. This is derived from the lower-bound on the Dart SDK requirement
/// in the `pubspec.yaml` for the given package.
LanguageVersion? languageVersion;
/// Additional properties not in the specification for the
/// `.dart_tool/package_config.json` file.
Map<String, dynamic>? additionalProperties;
PackageConfigEntry({
required this.name,
required this.rootUri,
this.packageUri,
this.languageVersion,
this.additionalProperties = const {},
});
/// Create [PackageConfigEntry] from JSON [data].
///
/// Throws [FormatException], if format is invalid, this does not validate the
/// contents only that the format is correct.
factory PackageConfigEntry.fromJson(Object data) {
if (data is! Map<String, dynamic>) {
throw FormatException(
'packages[] entries in package_config.json must be JSON objects',
);
}
final root = data;
Never throwFormatException(String property, String mustBe) =>
throw FormatException(
'"packages[].$property" in .dart_tool/package_config.json $mustBe',
);
final name = root['name'];
if (name is! String) {
throwFormatException('name', 'must be a string');
}
final Uri rootUri;
final rootUriRaw = root['rootUri'];
if (rootUriRaw is! String) {
throwFormatException('rootUri', 'must be a string');
}
try {
rootUri = Uri.parse(rootUriRaw);
} on FormatException {
throwFormatException('rootUri', 'must be a URI');
}
Uri? packageUri;
var packageUriRaw = root['packageUri'];
if (packageUriRaw != null) {
if (packageUriRaw is! String) {
throwFormatException('packageUri', 'must be a string');
}
if (!packageUriRaw.endsWith('/')) {
packageUriRaw = '$packageUriRaw/';
}
try {
packageUri = Uri.parse(packageUriRaw);
} on FormatException {
throwFormatException('packageUri', 'must be a URI');
}
}
LanguageVersion? languageVersion;
final languageVersionRaw = root['languageVersion'];
if (languageVersionRaw != null) {
if (languageVersionRaw is! String) {
throwFormatException('languageVersion', 'must be a string');
}
try {
languageVersion = LanguageVersion.parse(languageVersionRaw);
} on FormatException {
throwFormatException(
'languageVersion',
'must be on the form <major>.<minor>',
);
}
}
return PackageConfigEntry(
name: name,
rootUri: rootUri,
packageUri: packageUri,
languageVersion: languageVersion,
);
}
/// Convert to JSON structure.
Map<String, Object?> toJson() => {
'name': name,
'rootUri': rootUri.toString(),
if (packageUri != null) 'packageUri': packageUri.toString(),
if (languageVersion != null) 'languageVersion': '$languageVersion',
}..addAll(additionalProperties ?? {});
@override
String toString() {
// TODO: implement toString
return JsonEncoder.withIndent(' ').convert(toJson());
}
String resolvedRootDir(String packageConfigPath) {
return p.join(p.dirname(packageConfigPath), p.fromUri(rootUri));
}
}