| // 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:pub_semver/pub_semver.dart'; | |
| import 'language_version.dart'; | |
| import 'path.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; | |
| /// 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.generator, | |
| this.generatorVersion, | |
| Map<String, dynamic>? additionalProperties, | |
| }) : additionalProperties = additionalProperties ?? {} { | |
| final names = <String>{}; | |
| // Sanity check: | |
| for (final p in packages) { | |
| if (!names.add(p.name)) { | |
| throw ArgumentError( | |
| 'Duplicate name ${p.name} in generated package config', | |
| ); | |
| } | |
| } | |
| } | |
| /// 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 const FormatException('package_config.json must be a JSON object'); | |
| } | |
| final root = data; | |
| Never 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 as Object)); | |
| } | |
| // Read the 'generator' property | |
| final generator = root['generator']; | |
| if (generator is! String?) { | |
| throw const 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, | |
| packages: packages, | |
| 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(), | |
| 'generator': generator, | |
| 'generatorVersion': generatorVersion?.toString(), | |
| }..addAll(additionalProperties); | |
| } | |
| 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; | |
| /// The experiments the package opts in to. | |
| /// | |
| /// Taken from the `experiments` field of the package's `pubspec.yaml`. Tools | |
| /// enable these experiments for the libraries of this package only. | |
| List<String> experiments; | |
| /// 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.experiments = const [], | |
| 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 const 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>', | |
| ); | |
| } | |
| } | |
| final experiments = switch (root['experiments']) { | |
| null => const <String>[], | |
| final List<Object?> list when list.every((e) => e is String) => | |
| list.cast<String>(), | |
| _ => throwFormatException('experiments', 'must be a list of strings'), | |
| }; | |
| return PackageConfigEntry( | |
| name: name, | |
| rootUri: rootUri, | |
| packageUri: packageUri, | |
| languageVersion: languageVersion, | |
| experiments: experiments, | |
| ); | |
| } | |
| /// Convert to JSON structure. | |
| Map<String, Object?> toJson() => { | |
| 'name': name, | |
| 'rootUri': rootUri.toString(), | |
| if (packageUri != null) 'packageUri': packageUri.toString(), | |
| if (languageVersion != null) 'languageVersion': '$languageVersion', | |
| if (experiments.isNotEmpty) 'experiments': experiments, | |
| }..addAll(additionalProperties ?? {}); | |
| @override | |
| String toString() { | |
| return const JsonEncoder.withIndent(' ').convert(toJson()); | |
| } | |
| String resolvedRootDir(String packageConfigPath) { | |
| return p.join(p.dirname(packageConfigPath), p.fromUri(rootUri)); | |
| } | |
| } |