Version 2.0.0 supporting package_config.json (#56)
Giving it the package name package_config_2 to avoid conflict with the existing package_config which is used by the `test` package.
This is the initial version. It will be updated to allow overlapping packages, as long as no file in the package URI root can be considered as belonging to a different package.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 50f1ede..84384fc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,6 @@
+## 2.0.0
+ - Based on new JSON file format with more content.
+
## 1.2.0
- Added support for writing default-package entries.
- Fixed bug when writing `Uri`s containing a fragment.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 6f5e0ea..8423ff9 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -23,7 +23,7 @@
### File headers
All files in the project must start with the following header.
- // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
+ // 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.
diff --git a/LICENSE b/LICENSE
index de31e1a..f75d7c2 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright 2015, the Dart project authors. All rights reserved.
+Copyright 2019, the Dart project authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
diff --git a/lib/discovery.dart b/lib/discovery.dart
deleted file mode 100644
index 57584b6..0000000
--- a/lib/discovery.dart
+++ /dev/null
@@ -1,230 +0,0 @@
-// Copyright (c) 2015, 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.
-
-library package_config.discovery;
-
-import "dart:async";
-import "dart:io";
-import "dart:typed_data" show Uint8List;
-
-import "package:path/path.dart" as path;
-
-import "packages.dart";
-import "packages_file.dart" as pkgfile show parse;
-import "src/packages_impl.dart";
-import "src/packages_io_impl.dart";
-
-/// Reads a package resolution file and creates a [Packages] object from it.
-///
-/// The [packagesFile] must exist and be loadable.
-/// Currently that means the URI must have a `file`, `http` or `https` scheme,
-/// and that the file can be loaded and its contents parsed correctly.
-///
-/// If the [loader] is provided, it is used to fetch non-`file` URIs, and
-/// it can support other schemes or set up more complex HTTP requests.
-///
-/// This function can be used to load an explicitly configured package
-/// resolution file, for example one specified using a `--packages`
-/// command-line parameter.
-Future<Packages> loadPackagesFile(Uri packagesFile,
- {Future<List<int>> loader(Uri uri)}) async {
- Packages parseBytes(List<int> bytes) {
- Map<String, Uri> packageMap = pkgfile.parse(bytes, packagesFile);
- return new MapPackages(packageMap);
- }
-
- if (packagesFile.scheme == "file") {
- File file = new File.fromUri(packagesFile);
- return parseBytes(await file.readAsBytes());
- }
- if (loader == null) {
- return parseBytes(await _httpGet(packagesFile));
- }
- return parseBytes(await loader(packagesFile));
-}
-
-/// Create a [Packages] object for a package directory.
-///
-/// The [packagesDir] URI should refer to a directory.
-/// Package names are resolved as relative to sub-directories of the
-/// package directory.
-///
-/// This function can be used for explicitly configured package directories,
-/// for example one specified using a `--package-root` comand-line parameter.
-Packages getPackagesDirectory(Uri packagesDir) {
- if (packagesDir.scheme == "file") {
- Directory directory = new Directory.fromUri(packagesDir);
- return new FilePackagesDirectoryPackages(directory);
- }
- if (!packagesDir.path.endsWith('/')) {
- packagesDir = packagesDir.replace(path: packagesDir.path + '/');
- }
- return new NonFilePackagesDirectoryPackages(packagesDir);
-}
-
-/// Discover the package configuration for a Dart script.
-///
-/// The [baseUri] points to either the Dart script or its directory.
-/// A package resolution strategy is found by going through the following steps,
-/// and stopping when something is found.
-///
-/// * Check if a `.packages` file exists in the same directory.
-/// * If `baseUri`'s scheme is not `file`, then assume a `packages` directory
-/// in the same directory, and resolve packages relative to that.
-/// * If `baseUri`'s scheme *is* `file`:
-/// * Check if a `packages` directory exists.
-/// * Otherwise check each successive parent directory of `baseUri` for a
-/// `.packages` file.
-///
-/// If any of these tests succeed, a `Packages` class is returned.
-/// Returns the constant [noPackages] if no resolution strategy is found.
-///
-/// This function currently only supports `file`, `http` and `https` URIs.
-/// It needs to be able to load a `.packages` file from the URI, so only
-/// recognized schemes are accepted.
-///
-/// To support other schemes, or more complex HTTP requests,
-/// an optional [loader] function can be supplied.
-/// It's called to load the `.packages` file for a non-`file` scheme.
-/// The loader function returns the *contents* of the file
-/// identified by the URI it's given.
-/// The content should be a UTF-8 encoded `.packages` file, and must return an
-/// error future if loading fails for any reason.
-Future<Packages> findPackages(Uri baseUri,
- {Future<List<int>> loader(Uri unsupportedUri)}) {
- if (baseUri.scheme == "file") {
- return new Future<Packages>.sync(() => findPackagesFromFile(baseUri));
- } else if (loader != null) {
- return findPackagesFromNonFile(baseUri, loader: loader);
- } else if (baseUri.scheme == "http" || baseUri.scheme == "https") {
- return findPackagesFromNonFile(baseUri, loader: _httpGet);
- } else {
- return new Future<Packages>.value(Packages.noPackages);
- }
-}
-
-/// Find the location of the package resolution file/directory for a Dart file.
-///
-/// Checks for a `.packages` file in the [workingDirectory].
-/// If not found, checks for a `packages` directory in the same directory.
-/// If still not found, starts checking parent directories for
-/// `.packages` until reaching the root directory.
-///
-/// Returns a [File] object of a `.packages` file if one is found, or a
-/// [Directory] object for the `packages/` directory if that is found.
-FileSystemEntity _findPackagesFile(String workingDirectory) {
- var dir = new Directory(workingDirectory);
- if (!dir.isAbsolute) dir = dir.absolute;
- if (!dir.existsSync()) {
- throw new ArgumentError.value(
- workingDirectory, "workingDirectory", "Directory does not exist.");
- }
- File checkForConfigFile(Directory directory) {
- assert(directory.isAbsolute);
- var file = new File(path.join(directory.path, ".packages"));
- if (file.existsSync()) return file;
- return null;
- }
-
- // Check for $cwd/.packages
- var packagesCfgFile = checkForConfigFile(dir);
- if (packagesCfgFile != null) return packagesCfgFile;
- // Check for $cwd/packages/
- var packagesDir = new Directory(path.join(dir.path, "packages"));
- if (packagesDir.existsSync()) return packagesDir;
- // Check for cwd(/..)+/.packages
- var parentDir = dir.parent;
- while (parentDir.path != dir.path) {
- packagesCfgFile = checkForConfigFile(parentDir);
- if (packagesCfgFile != null) break;
- dir = parentDir;
- parentDir = dir.parent;
- }
- return packagesCfgFile;
-}
-
-/// Finds a package resolution strategy for a local Dart script.
-///
-/// The [fileBaseUri] points to either a Dart script or the directory of the
-/// script. The `fileBaseUri` must be a `file:` URI.
-///
-/// This function first tries to locate a `.packages` file in the `fileBaseUri`
-/// directory. If that is not found, it instead checks for the presence of
-/// a `packages/` directory in the same place.
-/// If that also fails, it starts checking parent directories for a `.packages`
-/// file, and stops if it finds it.
-/// Otherwise it gives up and returns [Packages.noPackages].
-Packages findPackagesFromFile(Uri fileBaseUri) {
- Uri baseDirectoryUri = fileBaseUri;
- if (!fileBaseUri.path.endsWith('/')) {
- baseDirectoryUri = baseDirectoryUri.resolve(".");
- }
- String baseDirectoryPath = baseDirectoryUri.toFilePath();
- FileSystemEntity location = _findPackagesFile(baseDirectoryPath);
- if (location == null) return Packages.noPackages;
- if (location is File) {
- List<int> fileBytes = location.readAsBytesSync();
- Map<String, Uri> map =
- pkgfile.parse(fileBytes, new Uri.file(location.path));
- return new MapPackages(map);
- }
- assert(location is Directory);
- return new FilePackagesDirectoryPackages(location);
-}
-
-/// Finds a package resolution strategy for a Dart script.
-///
-/// The [nonFileUri] points to either a Dart script or the directory of the
-/// script.
-/// The [nonFileUri] should not be a `file:` URI since the algorithm for
-/// finding a package resolution strategy is more elaborate for `file:` URIs.
-/// In that case, use [findPackagesFromFile].
-///
-/// This function first tries to locate a `.packages` file in the [nonFileUri]
-/// directory. If that is not found, it instead assumes a `packages/` directory
-/// in the same place.
-///
-/// By default, this function only works for `http:` and `https:` URIs.
-/// To support other schemes, a loader must be provided, which is used to
-/// try to load the `.packages` file. The loader should return the contents
-/// of the requested `.packages` file as bytes, which will be assumed to be
-/// UTF-8 encoded.
-Future<Packages> findPackagesFromNonFile(Uri nonFileUri,
- {Future<List<int>> loader(Uri name)}) async {
- if (loader == null) loader = _httpGet;
- Uri packagesFileUri = nonFileUri.resolve(".packages");
-
- try {
- List<int> fileBytes = await loader(packagesFileUri);
- Map<String, Uri> map = pkgfile.parse(fileBytes, packagesFileUri);
- return new MapPackages(map);
- } catch (_) {
- // Didn't manage to load ".packages". Assume a "packages/" directory.
- Uri packagesDirectoryUri = nonFileUri.resolve("packages/");
- return new NonFilePackagesDirectoryPackages(packagesDirectoryUri);
- }
-}
-
-/// Fetches a file over http.
-Future<List<int>> _httpGet(Uri uri) async {
- HttpClient client = new HttpClient();
- HttpClientRequest request = await client.getUrl(uri);
- HttpClientResponse response = await request.close();
- if (response.statusCode != HttpStatus.ok) {
- throw new HttpException('${response.statusCode} ${response.reasonPhrase}',
- uri: uri);
- }
- List<List<int>> splitContent = await response.toList();
- int totalLength = 0;
- for (var list in splitContent) {
- totalLength += list.length;
- }
- Uint8List result = new Uint8List(totalLength);
- int offset = 0;
- for (List<int> contentPart in splitContent) {
- result.setRange(offset, offset + contentPart.length, contentPart);
- offset += contentPart.length;
- }
- return result;
-}
diff --git a/lib/discovery_analysis.dart b/lib/discovery_analysis.dart
deleted file mode 100644
index d623303..0000000
--- a/lib/discovery_analysis.dart
+++ /dev/null
@@ -1,167 +0,0 @@
-// Copyright (c) 2015, 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.
-
-/// Analyse a directory structure and find packages resolvers for each
-/// sub-directory.
-///
-/// The resolvers are generally the same that would be found by using
-/// the `discovery.dart` library on each sub-directory in turn,
-/// but more efficiently and with some heuristics for directories that
-/// wouldn't otherwise have a package resolution strategy, or that are
-/// determined to be "package directories" themselves.
-library package_config.discovery_analysis;
-
-import "dart:collection" show HashMap;
-import "dart:io" show File, Directory;
-
-import "package:path/path.dart" as path;
-
-import "packages.dart";
-import "packages_file.dart" as pkgfile;
-import "src/packages_impl.dart";
-import "src/packages_io_impl.dart";
-
-/// Associates a [Packages] package resolution strategy with a directory.
-///
-/// The package resolution applies to the directory and any sub-directory
-/// that doesn't have its own overriding child [PackageContext].
-abstract class PackageContext {
- /// The directory that introduced the [packages] resolver.
- Directory get directory;
-
- /// A [Packages] resolver that applies to the directory.
- ///
- /// Introduced either by a `.packages` file or a `packages/` directory.
- Packages get packages;
-
- /// Child contexts that apply to sub-directories of [directory].
- List<PackageContext> get children;
-
- /// Look up the [PackageContext] that applies to a specific directory.
- ///
- /// The directory must be inside [directory].
- PackageContext operator [](Directory directory);
-
- /// A map from directory to package resolver.
- ///
- /// Has an entry for this package context and for each child context
- /// contained in this one.
- Map<Directory, Packages> asMap();
-
- /// Analyze [directory] and sub-directories for package resolution strategies.
- ///
- /// Returns a mapping from sub-directories to [Packages] objects.
- ///
- /// The analysis assumes that there are no `.packages` files in a parent
- /// directory of `directory`. If there is, its corresponding `Packages` object
- /// should be provided as `root`.
- static PackageContext findAll(Directory directory,
- {Packages root: Packages.noPackages}) {
- if (!directory.existsSync()) {
- throw ArgumentError("Directory not found: $directory");
- }
- var contexts = <PackageContext>[];
- void findRoots(Directory directory) {
- Packages packages;
- List<PackageContext> oldContexts;
- File packagesFile = File(path.join(directory.path, ".packages"));
- if (packagesFile.existsSync()) {
- packages = _loadPackagesFile(packagesFile);
- oldContexts = contexts;
- contexts = [];
- } else {
- Directory packagesDir =
- Directory(path.join(directory.path, "packages"));
- if (packagesDir.existsSync()) {
- packages = FilePackagesDirectoryPackages(packagesDir);
- oldContexts = contexts;
- contexts = [];
- }
- }
- for (var entry in directory.listSync()) {
- if (entry is Directory) {
- if (packages == null || !entry.path.endsWith("/packages")) {
- findRoots(entry);
- }
- }
- }
- if (packages != null) {
- oldContexts.add(_PackageContext(directory, packages, contexts));
- contexts = oldContexts;
- }
- }
-
- findRoots(directory);
- // If the root is not itself context root, add a the wrapper context.
- if (contexts.length == 1 && contexts[0].directory == directory) {
- return contexts[0];
- }
- return _PackageContext(directory, root, contexts);
- }
-}
-
-class _PackageContext implements PackageContext {
- final Directory directory;
- final Packages packages;
- final List<PackageContext> children;
- _PackageContext(this.directory, this.packages, List<PackageContext> children)
- : children = List<PackageContext>.unmodifiable(children);
-
- Map<Directory, Packages> asMap() {
- var result = HashMap<Directory, Packages>();
- recurse(_PackageContext current) {
- result[current.directory] = current.packages;
- for (var child in current.children) {
- recurse(child);
- }
- }
-
- recurse(this);
- return result;
- }
-
- PackageContext operator [](Directory directory) {
- String path = directory.path;
- if (!path.startsWith(this.directory.path)) {
- throw ArgumentError("Not inside $path: $directory");
- }
- _PackageContext current = this;
- // The current path is know to agree with directory until deltaIndex.
- int deltaIndex = current.directory.path.length;
- List children = current.children;
- int i = 0;
- while (i < children.length) {
- // TODO(lrn): Sort children and use binary search.
- _PackageContext child = children[i];
- String childPath = child.directory.path;
- if (_stringsAgree(path, childPath, deltaIndex, childPath.length)) {
- deltaIndex = childPath.length;
- if (deltaIndex == path.length) {
- return child;
- }
- current = child;
- children = current.children;
- i = 0;
- continue;
- }
- i++;
- }
- return current;
- }
-
- static bool _stringsAgree(String a, String b, int start, int end) {
- if (a.length < end || b.length < end) return false;
- for (int i = start; i < end; i++) {
- if (a.codeUnitAt(i) != b.codeUnitAt(i)) return false;
- }
- return true;
- }
-}
-
-Packages _loadPackagesFile(File file) {
- var uri = Uri.file(file.path);
- var bytes = file.readAsBytesSync();
- var map = pkgfile.parse(bytes, uri);
- return MapPackages(map);
-}
diff --git a/lib/package_config.dart b/lib/package_config.dart
new file mode 100644
index 0000000..4d6f346
--- /dev/null
+++ b/lib/package_config.dart
@@ -0,0 +1,141 @@
+// 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.
+
+/// A package configuration is a way to assign file paths to package URIs,
+/// and vice-versa,
+library package_config.package_config;
+
+import "dart:io" show File, Directory;
+import "dart:typed_data" show Uint8List;
+import "src/discovery.dart" as discover;
+import "src/package_config.dart";
+import "src/package_config_json.dart";
+
+export "src/package_config.dart" show PackageConfig, Package;
+export "src/errors.dart" show PackageConfigError;
+
+/// Reads a specific package configuration file.
+///
+/// The file must exist and be readable.
+/// It must be either a valid `package_config.json` file
+/// or a valid `.packages` file.
+/// It is considered a `package_config.json` file if its first character
+/// is a `{`.
+///
+/// If the file is a `.packages` file, also checks if there is a
+/// `.dart_tool/package_config.json` file next to the original file,
+/// and if so, loads that instead.
+Future<PackageConfig> loadPackageConfig(File file) => readAnyConfigFile(file);
+
+/// Reads a specific package configuration URI.
+///
+/// The file of the URI must exist and be readable.
+/// It must be either a valid `package_config.json` file
+/// or a valid `.packages` file.
+/// It is considered a `package_config.json` file if its first
+/// non-whitespace character is a `{`.
+///
+/// If the file is a `.packages` file, first checks if there is a
+/// `.dart_tool/package_config.json` file next to the original file,
+/// and if so, loads that instead.
+/// The [file] *must not* be a `package:` URI.
+///
+/// If [loader] is provided, URIs are loaded using that function.
+/// The future returned by the loader must complete with a [Uint8List]
+/// containing the entire file content,
+/// or with `null` if the file does not exist.
+/// The loader may throw at its own discretion, for situations where
+/// it determines that an error might be need user attention,
+/// but it is always allowed to return `null`.
+/// This function makes no attempt to catch such errors.
+///
+/// If no [loader] is supplied, a default loader is used which
+/// only accepts `file:`, `http:` and `https:` URIs,
+/// and which uses the platform file system and HTTP requests to
+/// fetch file content. The default loader never throws because
+/// of an I/O issue, as long as the location URIs are valid.
+/// As such, it does not distinguish between a file not existing,
+/// and it being temporarily locked or unreachable.
+Future<PackageConfig> loadPackageConfigUri(Uri file,
+ {Future<Uint8List /*?*/ > loader(Uri uri) /*?*/}) =>
+ readAnyConfigFileUri(file, loader);
+
+/// Finds a package configuration relative to [directory].
+///
+/// If [directory] contains a package configuration,
+/// either a `.dart_tool/package_config.json` file or,
+/// if not, a `.packages`, then that file is loaded.
+///
+/// If no file is found in the current directory,
+/// then the parent directories are checked recursively,
+/// all the way to the root directory, to check if those contains
+/// a package configuration.
+/// If [recurse] is set to [false], this parent directory check is not
+/// performed.
+///
+/// Returns `null` if no configuration file is found.
+Future<PackageConfig> findPackageConfig(Directory directory,
+ {bool recurse = true}) =>
+ discover.findPackageConfig(directory, recurse);
+
+/// Finds a package configuration relative to [location].
+///
+/// If [location] contains a package configuration,
+/// either a `.dart_tool/package_config.json` file or,
+/// if not, a `.packages`, then that file is loaded.
+/// The [location] URI *must not* be a `package:` URI.
+/// It should be a hierarchical URI which is supported
+/// by [loader].
+///
+/// If no file is found in the current directory,
+/// then the parent directories are checked recursively,
+/// all the way to the root directory, to check if those contains
+/// a package configuration.
+/// If [recurse] is set to [false], this parent directory check is not
+/// performed.
+///
+/// If [loader] is provided, URIs are loaded using that function.
+/// The future returned by the loader must complete with a [Uint8List]
+/// containing the entire file content,
+/// or with `null` if the file does not exist.
+/// The loader may throw at its own discretion, for situations where
+/// it determines that an error might be need user attention,
+/// but it is always allowed to return `null`.
+/// This function makes no attempt to catch such errors.
+///
+/// If no [loader] is supplied, a default loader is used which
+/// only accepts `file:`, `http:` and `https:` URIs,
+/// and which uses the platform file system and HTTP requests to
+/// fetch file content. The default loader never throws because
+/// of an I/O issue, as long as the location URIs are valid.
+/// As such, it does not distinguish between a file not existing,
+/// and it being temporarily locked or unreachable.
+///
+/// Returns `null` if no configuration file is found.
+Future<PackageConfig> findPackageConfigUri(Uri location,
+ {bool recurse = true, Future<Uint8List /*?*/ > loader(Uri uri)}) =>
+ discover.findPackageConfigUri(location, loader, recurse);
+
+/// Writes a package configuration to the provided directory.
+///
+/// Writes `.dart_tool/package_config.json` relative to [directory].
+/// If the `.dart_tool/` directory does not exist, it is created.
+/// If it cannot be created, this operation fails.
+///
+/// If [extraData] contains any entries, they are added to the JSON
+/// written to the `package_config.json` file. Entries with the names
+/// `"configVersion"` or `"packages"` are ignored, all other entries
+/// are added verbatim.
+/// This is intended for, e.g., the
+/// `"generator"`, `"generated"` and `"generatorVersion"`
+/// properties.
+///
+/// Also writes a `.packages` file in [directory].
+/// This will stop happening eventually as the `.packages` file becomes
+/// discontinued.
+/// A comment is generated if `[PackageConfig.extraData]` contains a
+/// `"generator"` entry.
+Future<void> savePackageConfig(
+ PackageConfig configuration, Directory directory) =>
+ writePackageConfigJson(configuration, directory);
diff --git a/lib/packages.dart b/lib/packages.dart
deleted file mode 100644
index 886fbc8..0000000
--- a/lib/packages.dart
+++ /dev/null
@@ -1,94 +0,0 @@
-// Copyright (c) 2015, 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.
-
-library package_config.packages;
-
-import "src/packages_impl.dart";
-
-/// A package resolution strategy.
-///
-/// Allows converting a `package:` URI to a different kind of URI.
-///
-/// May also allow listing the available packages and converting
-/// to a `Map<String, Uri>` that gives the base location of each available
-/// package. In some cases there is no way to find the available packages,
-/// in which case [packages] and [asMap] will throw if used.
-/// One such case is if the packages are resolved relative to a
-/// `packages/` directory available over HTTP.
-abstract class Packages {
- /// A [Packages] resolver containing no packages.
- ///
- /// This constant object is returned by [find] above if no
- /// package resolution strategy is found.
- static const Packages noPackages = const NoPackages();
-
- /// Resolve a package URI into a non-package URI.
- ///
- /// Translates a `package:` URI, according to the package resolution
- /// strategy, into a URI that can be loaded.
- /// By default, only `file`, `http` and `https` URIs are returned.
- /// Custom `Packages` objects may return other URIs.
- ///
- /// If resolution fails because a package with the requested package name
- /// is not available, the [notFound] function is called.
- /// If no `notFound` function is provided, it defaults to throwing an error.
- ///
- /// The [packageUri] must be a valid package URI.
- Uri resolve(Uri packageUri, {Uri notFound(Uri packageUri)});
-
- /// Return the names of the available packages.
- ///
- /// Returns an iterable that allows iterating the names of available packages.
- ///
- /// Some `Packages` objects are unable to find the package names,
- /// and getting `packages` from such a `Packages` object will throw.
- Iterable<String> get packages;
-
- /// Retrieve metadata associated with a package.
- ///
- /// Metadata have string keys and values, and are looked up by key.
- ///
- /// Returns `null` if the argument is not a valid package name,
- /// or if the package is not one of the packages configured by
- /// this packages object, or if the package does not have associated
- /// metadata with the provided [key].
- ///
- /// Not all `Packages` objects can support metadata.
- /// Those will always return `null`.
- String packageMetadata(String packageName, String key);
-
- /// Retrieve metadata associated with a library.
- ///
- /// If [libraryUri] is a `package:` URI, the returned value
- /// is the same that would be returned by [packageMetadata] with
- /// the package's name and the same key.
- ///
- /// If [libraryUri] is not a `package:` URI, and this [Packages]
- /// object has a [defaultPackageName], then the [key] is looked
- /// up on the default package instead.
- ///
- /// Otherwise the result is `null`.
- String libraryMetadata(Uri libraryUri, String key);
-
- /// Return the names-to-base-URI mapping of the available packages.
- ///
- /// Returns a map from package name to a base URI.
- /// The [resolve] method will resolve a package URI with a specific package
- /// name to a path extending the base URI that this map gives for that
- /// package name.
- ///
- /// Some `Packages` objects are unable to find the package names,
- /// and calling `asMap` on such a `Packages` object will throw.
- Map<String, Uri> asMap();
-
- /// The name of the "default package".
- ///
- /// A default package is a package that *non-package* libraries
- /// may be considered part of for some purposes.
- ///
- /// The value is `null` if there is no default package.
- /// Not all implementations of [Packages] supports a default package,
- /// and will always have a `null` value for those.
- String get defaultPackageName;
-}
diff --git a/lib/packages_file.dart b/lib/packages_file.dart
deleted file mode 100644
index 1c35d5b..0000000
--- a/lib/packages_file.dart
+++ /dev/null
@@ -1,232 +0,0 @@
-// Copyright (c) 2015, 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.
-
-library package_config.packages_file;
-
-import "package:charcode/ascii.dart";
-
-import "src/util.dart" show isValidPackageName;
-
-/// Parses a `.packages` file into a map from package name to base URI.
-///
-/// The [source] is the byte content of a `.packages` file, assumed to be
-/// UTF-8 encoded. In practice, all significant parts of the file must be ASCII,
-/// so Latin-1 or Windows-1252 encoding will also work fine.
-///
-/// If the file content is available as a string, its [String.codeUnits] can
-/// be used as the `source` argument of this function.
-///
-/// The [baseLocation] is used as a base URI to resolve all relative
-/// URI references against.
-/// If the content was read from a file, `baseLocation` should be the
-/// location of that file.
-///
-/// If [allowDefaultPackage] is set to true, an entry with an empty package name
-/// is accepted. This entry does not correspond to a package, but instead
-/// represents a *default package* which non-package libraries may be considered
-/// part of in some cases. The value of that entry must be a valid package name.
-///
-/// Returns a simple mapping from package name to package location.
-/// If default package is allowed, the map maps the empty string to the default package's name.
-Map<String, Uri> parse(List<int> source, Uri baseLocation,
- {bool allowDefaultPackage = false}) {
- int index = 0;
- Map<String, Uri> result = <String, Uri>{};
- while (index < source.length) {
- bool isComment = false;
- int start = index;
- int separatorIndex = -1;
- int end = source.length;
- int char = source[index++];
- if (char == $cr || char == $lf) {
- continue;
- }
- if (char == $colon) {
- if (!allowDefaultPackage) {
- throw FormatException("Missing package name", source, index - 1);
- }
- separatorIndex = index - 1;
- }
- isComment = char == $hash;
- while (index < source.length) {
- char = source[index++];
- if (char == $colon && separatorIndex < 0) {
- separatorIndex = index - 1;
- } else if (char == $cr || char == $lf) {
- end = index - 1;
- break;
- }
- }
- if (isComment) continue;
- if (separatorIndex < 0) {
- throw FormatException("No ':' on line", source, index - 1);
- }
- var packageName = new String.fromCharCodes(source, start, separatorIndex);
- if (packageName.isEmpty
- ? !allowDefaultPackage
- : !isValidPackageName(packageName)) {
- throw FormatException("Not a valid package name", packageName, 0);
- }
- var packageValue =
- new String.fromCharCodes(source, separatorIndex + 1, end);
- Uri packageLocation;
- if (packageName.isEmpty) {
- if (!isValidPackageName(packageValue)) {
- throw FormatException(
- "Default package entry value is not a valid package name");
- }
- packageLocation = Uri(path: packageValue);
- } else {
- packageLocation = baseLocation.resolve(packageValue);
- if (!packageLocation.path.endsWith('/')) {
- packageLocation =
- packageLocation.replace(path: packageLocation.path + "/");
- }
- }
- if (result.containsKey(packageName)) {
- if (packageName.isEmpty) {
- throw FormatException(
- "More than one default package entry", source, start);
- }
- throw FormatException("Same package name occured twice", source, start);
- }
- result[packageName] = packageLocation;
- }
- return result;
-}
-
-/// Writes the mapping to a [StringSink].
-///
-/// If [comment] is provided, the output will contain this comment
-/// with `# ` in front of each line.
-/// Lines are defined as ending in line feed (`'\n'`). If the final
-/// line of the comment doesn't end in a line feed, one will be added.
-///
-/// If [baseUri] is provided, package locations will be made relative
-/// to the base URI, if possible, before writing.
-///
-/// If [allowDefaultPackage] is `true`, the [packageMapping] may contain an
-/// empty string mapping to the _default package name_.
-///
-/// All the keys of [packageMapping] must be valid package names,
-/// and the values must be URIs that do not have the `package:` scheme.
-void write(StringSink output, Map<String, Uri> packageMapping,
- {Uri baseUri, String comment, bool allowDefaultPackage = false}) {
- ArgumentError.checkNotNull(allowDefaultPackage, 'allowDefaultPackage');
-
- if (baseUri != null && !baseUri.isAbsolute) {
- throw new ArgumentError.value(baseUri, "baseUri", "Must be absolute");
- }
-
- if (comment != null) {
- var lines = comment.split('\n');
- if (lines.last.isEmpty) lines.removeLast();
- for (var commentLine in lines) {
- output.write('# ');
- output.writeln(commentLine);
- }
- } else {
- output.write("# generated by package:package_config at ");
- output.write(new DateTime.now());
- output.writeln();
- }
-
- packageMapping.forEach((String packageName, Uri uri) {
- // If [packageName] is empty then [uri] is the _default package name_.
- if (allowDefaultPackage && packageName.isEmpty) {
- final defaultPackageName = uri.toString();
- if (!isValidPackageName(defaultPackageName)) {
- throw ArgumentError.value(
- defaultPackageName,
- 'defaultPackageName',
- '"$defaultPackageName" is not a valid package name',
- );
- }
- output.write(':');
- output.write(defaultPackageName);
- output.writeln();
- return;
- }
- // Validate packageName.
- if (!isValidPackageName(packageName)) {
- throw new ArgumentError('"$packageName" is not a valid package name');
- }
- if (uri.scheme == "package") {
- throw new ArgumentError.value(
- "Package location must not be a package: URI", uri.toString());
- }
- output.write(packageName);
- output.write(':');
- // If baseUri provided, make uri relative.
- if (baseUri != null) {
- uri = _relativize(uri, baseUri);
- }
- if (!uri.path.endsWith('/')) {
- uri = uri.replace(path: uri.path + '/');
- }
- output.write(uri);
- output.writeln();
- });
-}
-
-/// Attempts to return a relative URI for [uri].
-///
-/// The result URI satisfies `baseUri.resolveUri(result) == uri`,
-/// but may be relative.
-/// The `baseUri` must be absolute.
-Uri _relativize(Uri uri, Uri baseUri) {
- assert(baseUri.isAbsolute);
- if (uri.hasQuery || uri.hasFragment) {
- uri = new Uri(
- scheme: uri.scheme,
- userInfo: uri.hasAuthority ? uri.userInfo : null,
- host: uri.hasAuthority ? uri.host : null,
- port: uri.hasAuthority ? uri.port : null,
- path: uri.path);
- }
-
- // Already relative. We assume the caller knows what they are doing.
- if (!uri.isAbsolute) return uri;
-
- if (baseUri.scheme != uri.scheme) {
- return uri;
- }
-
- // If authority differs, we could remove the scheme, but it's not worth it.
- if (uri.hasAuthority != baseUri.hasAuthority) return uri;
- if (uri.hasAuthority) {
- if (uri.userInfo != baseUri.userInfo ||
- uri.host.toLowerCase() != baseUri.host.toLowerCase() ||
- uri.port != baseUri.port) {
- return uri;
- }
- }
-
- baseUri = baseUri.normalizePath();
- List<String> base = baseUri.pathSegments.toList();
- if (base.isNotEmpty) {
- base = new List<String>.from(base)..removeLast();
- }
- uri = uri.normalizePath();
- List<String> target = uri.pathSegments.toList();
- if (target.isNotEmpty && target.last.isEmpty) target.removeLast();
- int index = 0;
- while (index < base.length && index < target.length) {
- if (base[index] != target[index]) {
- break;
- }
- index++;
- }
- if (index == base.length) {
- if (index == target.length) {
- return new Uri(path: "./");
- }
- return new Uri(path: target.skip(index).join('/'));
- } else if (index > 0) {
- return new Uri(
- path: '../' * (base.length - index) + target.skip(index).join('/'));
- } else {
- return uri;
- }
-}
diff --git a/lib/src/discovery.dart b/lib/src/discovery.dart
new file mode 100644
index 0000000..6b0fc8f
--- /dev/null
+++ b/lib/src/discovery.dart
@@ -0,0 +1,132 @@
+// 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:io";
+import 'dart:typed_data';
+
+import "package:path/path.dart" as path;
+
+import "errors.dart";
+import "package_config_impl.dart";
+import "package_config_json.dart";
+import "packages_file.dart" as packages_file;
+import "util.dart" show defaultLoader;
+
+final Uri packageConfigJsonPath = Uri(path: ".dart_tool/package_config.json");
+final Uri dotPackagesPath = Uri(path: ".packages");
+final Uri currentPath = Uri(path: ".");
+final Uri parentPath = Uri(path: "..");
+
+/// Discover the package configuration for a Dart script.
+///
+/// The [baseDirectory] points to the directory of the Dart script.
+/// A package resolution strategy is found by going through the following steps,
+/// and stopping when something is found.
+///
+/// * Check if a `.dart_tool/package_config.json` file exists in the directory.
+/// * Check if a `.packages` file exists in the directory.
+/// * Repeat these checks for the parent directories until reaching the
+/// root directory if [recursive] is true.
+///
+/// If any of these tests succeed, a `PackageConfig` class is returned.
+/// Returns `null` if no configuration was found. If a configuration
+/// is needed, then the caller can supply [PackageConfig.empty].
+Future<PackageConfig /*?*/ > findPackageConfig(
+ Directory baseDirectory, bool recursive) async {
+ var directory = baseDirectory;
+ if (!directory.isAbsolute) directory = directory.absolute;
+ if (!await directory.exists()) {
+ return null;
+ }
+ do {
+ // Check for $cwd/.packages
+ var packageConfig = await findPackagConfigInDirectory(directory);
+ if (packageConfig != null) return packageConfig;
+ if (!recursive) break;
+ // Check in parent directories.
+ var parentDirectory = directory.parent;
+ if (parentDirectory.path == directory.path) break;
+ directory = parentDirectory;
+ } while (true);
+ return null;
+}
+
+/// Similar to [findPackageConfig] but based on a URI.
+Future<PackageConfig /*?*/ > findPackageConfigUri(Uri location,
+ Future<Uint8List /*?*/ > loader(Uri uri) /*?*/, bool recursive) async {
+ if (location.isScheme("package")) {
+ throw PackageConfigArgumentError(
+ location, "location", "Must not be a package: URI");
+ }
+ if (loader == null) {
+ if (location.isScheme("file")) {
+ return findPackageConfig(
+ Directory.fromUri(location.resolveUri(currentPath)), recursive);
+ }
+ loader = defaultLoader;
+ }
+ if (!location.path.endsWith("/")) location = location.resolveUri(currentPath);
+ while (true) {
+ var file = location.resolveUri(packageConfigJsonPath);
+ var bytes = await loader(file);
+ if (bytes != null) {
+ return parsePackageConfigBytes(bytes, file);
+ }
+ file = location.resolveUri(dotPackagesPath);
+ bytes = await loader(file);
+ if (bytes != null) {
+ return packages_file.parse(bytes, file);
+ }
+ if (!recursive) break;
+ var parent = location.resolveUri(parentPath);
+ if (parent == location) break;
+ location = parent;
+ }
+ return null;
+}
+
+/// Finds a `.packages` or `.dart_tool/package_config.json` file in [directory].
+///
+/// Loads the file, if it is there, and returns the resulting [PackageConfig].
+/// Returns `null` if the file isn't there.
+/// Throws [FormatException] if a file is there but is not valid.
+///
+/// If [extraData] is supplied and the `package_config.json` contains extra
+/// entries in the top JSON object, those extra entries are stored into
+/// [extraData].
+Future<PackageConfig /*?*/ > findPackagConfigInDirectory(
+ Directory directory) async {
+ var packageConfigFile = await checkForPackageConfigJsonFile(directory);
+ if (packageConfigFile != null) {
+ return await readPackageConfigJsonFile(packageConfigFile);
+ }
+ packageConfigFile = await checkForDotPackagesFile(directory);
+ if (packageConfigFile != null) {
+ return await readDotPackagesFile(packageConfigFile);
+ }
+ return null;
+}
+
+Future<File> /*?*/ checkForPackageConfigJsonFile(Directory directory) async {
+ assert(directory.isAbsolute);
+ var file =
+ File(path.join(directory.path, ".dart_tool", "package_config.json"));
+ if (await file.exists()) return file;
+ return null;
+}
+
+Future<File /*?*/ > checkForDotPackagesFile(Directory directory) async {
+ var file = File(path.join(directory.path, ".packages"));
+ if (await file.exists()) return file;
+ return null;
+}
+
+Future<Uint8List /*?*/ > _loadFile(File file) async {
+ Uint8List bytes;
+ try {
+ return await file.readAsBytes();
+ } catch (_) {
+ return null;
+ }
+}
diff --git a/lib/src/errors.dart b/lib/src/errors.dart
new file mode 100644
index 0000000..6c31cce
--- /dev/null
+++ b/lib/src/errors.dart
@@ -0,0 +1,24 @@
+// 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.
+
+/// General superclass of most errors and exceptions thrown by this package.
+///
+/// Only covers errors thrown while parsing package configuration files.
+/// Programming errors and I/O exceptions are not covered.
+abstract class PackageConfigError {
+ PackageConfigError._();
+}
+
+class PackageConfigArgumentError extends ArgumentError
+ implements PackageConfigError {
+ PackageConfigArgumentError(Object /*?*/ value, String name, String message)
+ : super.value(value, name, message);
+}
+
+class PackageConfigFormatException extends FormatException
+ implements PackageConfigError {
+ PackageConfigFormatException(String message, Object /*?*/ value,
+ [int /*?*/ index])
+ : super(message, value, index);
+}
diff --git a/lib/src/package_config.dart b/lib/src/package_config.dart
new file mode 100644
index 0000000..f7b96b8
--- /dev/null
+++ b/lib/src/package_config.dart
@@ -0,0 +1,176 @@
+// 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 "package_config_impl.dart";
+
+/// A package configuration.
+///
+/// Associates configuration data to packages and files in packages.
+///
+/// More members may be added to this class in the future,
+/// so classes outside of this package must not implement [PackageConfig]
+/// or any subclass of it.
+abstract class PackageConfig {
+ /// The largest configuration version currently recognized.
+ static const int maxVersion = 2;
+
+ /// An empty package configuration.
+ ///
+ /// A package configuration with no available packages.
+ /// Is used as a default value where a package configuration
+ /// is expected, but none have been specified or found.
+ static const PackageConfig empty = const SimplePackageConfig.empty();
+
+ /// Creats a package configuration with the provided available [packages].
+ ///
+ /// The packages must be valid packages (valid package name, valid
+ /// absolute directory URIs, valid language version, if any),
+ /// and there must not be two packages with the same name or with
+ /// overlapping root directories.
+ ///
+ /// If supplied, the [extraData] will be available as the
+ /// [PackageConfig.extraData] of the created configuration.
+ ///
+ /// The version of the resulting configuration is always [maxVersion].
+ factory PackageConfig(Iterable<Package> packages, {dynamic extraData}) =>
+ SimplePackageConfig(maxVersion, packages);
+
+ /// The configuration version number.
+ ///
+ /// Currently this is 1 or 2, where
+ /// * Version one is the `.packages` file format and
+ /// * Version two is the first `package_config.json` format.
+ ///
+ /// Instances of this class supports both, and the version
+ /// is only useful for detecting which kind of file the configuration
+ /// was read from.
+ int get version;
+
+ /// All the available packages of this configuration.
+ ///
+ /// No two of these packages have the same name,
+ /// and no two [Package.root] directories overlap.
+ Iterable<Package> get packages;
+
+ /// Look up a package by name.
+ ///
+ /// Returns the [Package] fron [packages] with [packageName] as
+ /// [Package.name]. Returns `null` if the package is not available in the
+ /// current configuration.
+ Package /*?*/ operator [](String packageName);
+
+ /// Provides the associated package for a specific [file] (or directory).
+ ///
+ /// Returns a [Package] which contains the [file]'s path, if any.
+ /// That is, the [Package.rootUri] directory is a parent directory
+ /// of the [file]'s location.
+ ///
+ /// Returns `null` if the file does not belong to any package.
+ Package /*?*/ packageOf(Uri file);
+
+ /// Resolves a `package:` URI to a non-package URI
+ ///
+ /// The [packageUri] must be a valid package URI. That means:
+ /// * A URI with `package` as scheme,
+ /// * with no authority part (`package://...`),
+ /// * with a path starting with a valid package name followed by a slash, and
+ /// * with no query or fragment part.
+ ///
+ /// Throws an [ArgumentError] (which also implements [PackageConfigError])
+ /// if the package URI is not valid.
+ ///
+ /// Returns `null` if the package name of [packageUri] is not available
+ /// in this package configuration.
+ /// Returns the remaining path of the package URI resolved relative to the
+ /// [Package.packageUriRoot] of the corresponding package.
+ Uri /*?*/ resolve(Uri packageUri);
+
+ /// The package URI which resolves to [nonPackageUri].
+ ///
+ /// The [nonPackageUri] must not have any query or fragment part,
+ /// and it must not have `package` as scheme.
+ /// Throws an [ArgumentError] (which also implements [PackageConfigError])
+ /// if the non-package URI is not valid.
+ ///
+ /// Returns a package URI which [resolve] will convert to [nonPackageUri],
+ /// if any such URI exists. Returns `null` if no such package URI exists.
+ Uri /*?*/ toPackageUri(Uri nonPackageUri);
+
+ /// Extra data associated with the package configuration.
+ ///
+ /// The data may be in any format, depending on who introduced it.
+ /// The standard `packjage_config.json` file storage will only store
+ /// JSON-like list/map data structures.
+ dynamic get extraData;
+}
+
+/// Configuration data for a single package.
+abstract class Package {
+ /// Creates a package with the provided properties.
+ ///
+ /// The [name] must be a valid package name.
+ /// The [root] must be an absolute directory URI, meaning an absolute URI
+ /// with no query or fragment path and a path starting and ending with `/`.
+ /// The [packageUriRoot], if provided, must be either an absolute
+ /// directory URI or a relative URI reference which is then resolved
+ /// relative to [root]. It must then also be a subdirectory of [root],
+ /// or the same directory.
+ /// If [languageVersion] is supplied, it must be a valid Dart language
+ /// version, which means two decimal integer literals separated by a `.`,
+ /// where the integer literals have no leading zeros unless they are
+ /// a single zero digit.
+ /// If [extraData] is supplied, it will be available as the
+ /// [Package.extraData] of the created package.
+ factory Package(String name, Uri root,
+ {Uri /*?*/ packageUriRoot,
+ String /*?*/ languageVersion,
+ dynamic extraData}) =>
+ SimplePackage(name, root, packageUriRoot, languageVersion, extraData);
+
+ /// The package-name of the package.
+ String get name;
+
+ /// The location of the root of the package.
+ ///
+ /// Is always an absolute URI with no query or fragment parts,
+ /// and with a path ending in `/`.
+ ///
+ /// All files in the [rootUri] directory are considered
+ /// part of the package for purposes where that that matters.
+ Uri get root;
+
+ /// The root of the files available through `package:` URIs.
+ ///
+ /// A `package:` URI with [name] as the package name is
+ /// resolved relative to this location.
+ ///
+ /// Is always an absolute URI with no query or fragment part
+ /// with a path ending in `/`,
+ /// and with a location which is a subdirectory
+ /// of the [root], or the same as the [root].
+ Uri get packageUriRoot;
+
+ /// The default language version associated with this package.
+ ///
+ /// Each package may have a default language version associated,
+ /// which is the language version used to parse and compile
+ /// Dart files in the package.
+ /// A package version is always of the form:
+ ///
+ /// * A numeral consisting of one or more decimal digits,
+ /// with no leading zero unless the entire numeral is a single zero digit.
+ /// * Followed by a `.` character.
+ /// * Followed by another numeral of the same form.
+ ///
+ /// There is no whitespace allowed around the numerals.
+ /// Valid version numbers include `2.5`, `3.0`, and `1234.5678`.
+ String /*?*/ get languageVersion;
+
+ /// Extra data associated with the specific package.
+ ///
+ /// The data may be in any format, depending on who introduced it.
+ /// The standard `packjage_config.json` file storage will only store
+ /// JSON-like list/map data structures.
+ dynamic get extraData;
+}
diff --git a/lib/src/package_config_impl.dart b/lib/src/package_config_impl.dart
new file mode 100644
index 0000000..0bbe18f
--- /dev/null
+++ b/lib/src/package_config_impl.dart
@@ -0,0 +1,181 @@
+// 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 'errors.dart';
+import "package_config.dart";
+export "package_config.dart";
+import "util.dart";
+
+class SimplePackageConfig implements PackageConfig {
+ final int version;
+ final Map<String, Package> _packages;
+ final dynamic extraData;
+
+ SimplePackageConfig(int version, Iterable<Package> packages, [this.extraData])
+ : version = _validateVersion(version),
+ _packages = _validatePackages(packages);
+
+ SimplePackageConfig._(
+ int version, Iterable<SimplePackage> packages, this.extraData)
+ : version = _validateVersion(version),
+ _packages = {for (var package in packages) package.name: package};
+
+ /// Creates empty configuration.
+ ///
+ /// The empty configuration can be used in cases where no configuration is
+ /// found, but code expects a non-null configuration.
+ const SimplePackageConfig.empty()
+ : version = 1,
+ _packages = const <String, Package>{},
+ extraData = null;
+
+ static int _validateVersion(int version) {
+ if (version < 0 || version > PackageConfig.maxVersion) {
+ throw PackageConfigArgumentError(version, "version",
+ "Must be in the range 1 to ${PackageConfig.maxVersion}");
+ }
+ return version;
+ }
+
+ static Map<String, Package> _validatePackages(Iterable<Package> packages) {
+ Map<String, Package> result = {};
+ for (var package in packages) {
+ if (package is! SimplePackage) {
+ // SimplePackage validates these properties.
+ try {
+ _validatePackageData(package.name, package.root,
+ package.packageUriRoot, package.languageVersion);
+ } catch (e) {
+ throw PackageConfigArgumentError(
+ packages, "packages", "Package ${package.name}: ${e.message}");
+ }
+ }
+ var name = package.name;
+ if (result.containsKey(name)) {
+ throw PackageConfigArgumentError(
+ name, "packages", "Duplicate package name");
+ }
+ result[name] = package;
+ }
+
+ // Check that no root URI is a prefix of another.
+ if (result.length > 1) {
+ // Uris cache their toString, so this is not as bad as it looks.
+ var rootUris = [...result.values]
+ ..sort((a, b) => a.root.toString().compareTo(b.root.toString()));
+ var prev = rootUris[0];
+ var prevRoot = prev.root.toString();
+ for (int i = 1; i < rootUris.length; i++) {
+ var next = rootUris[i];
+ var nextRoot = next.root.toString();
+ // If one string is a prefix of another,
+ // the former sorts just before the latter.
+ if (nextRoot.startsWith(prevRoot)) {
+ throw PackageConfigArgumentError(
+ packages,
+ "packages",
+ "Package ${next.name} root overlaps "
+ "package ${prev.name} root.\n"
+ "${prev.name} root: $prevRoot\n"
+ "${next.name} root: $nextRoot\n");
+ }
+ prev = next;
+ }
+ }
+ return result;
+ }
+
+ Iterable<Package> get packages => _packages.values;
+
+ Package /*?*/ operator [](String packageName) => _packages[packageName];
+
+ /// Provides the associated package for a specific [file] (or directory).
+ ///
+ /// Returns a [Package] which contains the [file]'s path.
+ /// That is, the [Package.rootUri] directory is a parent directory
+ /// of the [file]'s location.
+ /// Returns `null` if the file does not belong to any package.
+ Package /*?*/ packageOf(Uri file) {
+ String path = file.toString();
+ for (var package in _packages.values) {
+ var rootPath = package.root.toString();
+ if (path.startsWith(rootPath)) return package;
+ }
+ return null;
+ }
+
+ Uri /*?*/ resolve(Uri packageUri) {
+ String packageName = checkValidPackageUri(packageUri, "packageUri");
+ return _packages[packageName]?.packageUriRoot?.resolveUri(
+ Uri(path: packageUri.path.substring(packageName.length + 1)));
+ }
+
+ Uri /*?*/ toPackageUri(Uri nonPackageUri) {
+ if (nonPackageUri.isScheme("package")) {
+ throw PackageConfigArgumentError(
+ nonPackageUri, "nonPackageUri", "Must not be a package URI");
+ }
+ if (nonPackageUri.hasQuery || nonPackageUri.hasFragment) {
+ throw PackageConfigArgumentError(nonPackageUri, "nonPackageUri",
+ "Must not have query or fragment part");
+ }
+ for (var package in _packages.values) {
+ var root = package.packageUriRoot;
+ if (isUriPrefix(root, nonPackageUri)) {
+ var rest = nonPackageUri.toString().substring(root.toString().length);
+ return Uri(scheme: "package", path: "${package.name}/$rest");
+ }
+ }
+ return null;
+ }
+}
+
+/// Configuration data for a single package.
+class SimplePackage implements Package {
+ final String name;
+ final Uri root;
+ final Uri packageUriRoot;
+ final String /*?*/ languageVersion;
+ final dynamic extraData;
+
+ SimplePackage._(this.name, this.root, this.packageUriRoot,
+ this.languageVersion, this.extraData);
+
+ factory SimplePackage(String name, Uri root, Uri packageUriRoot,
+ String /*?*/ languageVersion, dynamic extraData) {
+ _validatePackageData(name, root, packageUriRoot, languageVersion);
+ return SimplePackage._(
+ name, root, packageUriRoot, languageVersion, extraData);
+ }
+}
+
+void _validatePackageData(
+ String name, Uri root, Uri packageUriRoot, String /*?*/ languageVersion) {
+ if (!isValidPackageName(name)) {
+ throw PackageConfigArgumentError(name, "name", "Not a valid package name");
+ }
+ if (!isAbsoluteDirectoryUri(root)) {
+ throw PackageConfigArgumentError(
+ "$root",
+ "root",
+ "Not an absolute URI with no query or fragment "
+ "with a path ending in /");
+ }
+ if (!isAbsoluteDirectoryUri(packageUriRoot)) {
+ throw PackageConfigArgumentError(
+ packageUriRoot,
+ "packageUriRoot",
+ "Not an absolute URI with no query or fragment "
+ "with a path ending in /");
+ }
+ if (!isUriPrefix(root, packageUriRoot)) {
+ throw PackageConfigArgumentError(packageUriRoot, "packageUriRoot",
+ "The package URI root is not below the package root");
+ }
+ if (languageVersion != null &&
+ checkValidVersionNumber(languageVersion) >= 0) {
+ throw PackageConfigArgumentError(
+ languageVersion, "languageVersion", "Invalid language version format");
+ }
+}
diff --git a/lib/src/package_config_json.dart b/lib/src/package_config_json.dart
new file mode 100644
index 0000000..8a6014c
--- /dev/null
+++ b/lib/src/package_config_json.dart
@@ -0,0 +1,327 @@
+// 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 "dart:io";
+import "dart:typed_data";
+
+import 'package:charcode/ascii.dart';
+import "package:path/path.dart" as path;
+
+import "discovery.dart" show packageConfigJsonPath;
+import "errors.dart";
+import "package_config_impl.dart";
+import "packages_file.dart" as packages_file;
+import "util.dart";
+
+const String _configVersionKey = "configVersion";
+const String _packagesKey = "packages";
+const List<String> _topNames = [_configVersionKey, _packagesKey];
+const String _nameKey = "name";
+const String _rootUriKey = "rootUri";
+const String _packageUriKey = "packageUri";
+const String _languageVersionKey = "languageVersion";
+const List<String> _packageNames = [
+ _nameKey,
+ _rootUriKey,
+ _packageUriKey,
+ _languageVersionKey
+];
+
+const String _generatedKey = "generated";
+const String _generatorKey = "generator";
+const String _generatorVersionKey = "generatorVersion";
+
+/// Reads a package configuration file.
+///
+/// Detects whether the [file] is a version one `.packages` file or
+/// a version two `package_config.json` file.
+///
+/// If the [file] is a `.packages` file, first checks whether there is an
+/// adjacent `.dart_tool/package_config.json` file, and if so,
+/// reads that instead.
+///
+/// The file must exist and be a normal file.
+Future<PackageConfig> readAnyConfigFile(File file) async {
+ var bytes = await file.readAsBytes();
+ int firstChar = firstNonWhitespaceChar(bytes);
+ if (firstChar != $lbrace) {
+ // Definitely not a JSON object, probably a .packages.
+ var alternateFile = File(path.join(
+ path.dirname(file.path), ".dart_tool", "package_config.json"));
+ if (!alternateFile.existsSync()) {
+ return packages_file.parse(bytes, file.uri);
+ }
+ file = alternateFile;
+ bytes = await alternateFile.readAsBytes();
+ }
+ return parsePackageConfigBytes(bytes, file.uri);
+}
+
+/// Like [readAnyConfigFile] but uses a URI and an optional loader.
+Future<PackageConfig> readAnyConfigFileUri(
+ Uri file, Future<Uint8List /*?*/ > loader(Uri uri) /*?*/) async {
+ if (file.isScheme("package")) {
+ throw PackageConfigArgumentError(
+ file, "file", "Must not be a package: URI");
+ }
+ if (loader == null) {
+ if (file.isScheme("file")) return readAnyConfigFile(File.fromUri(file));
+ loader = defaultLoader;
+ }
+ var bytes = await loader(file);
+ if (bytes == null) {
+ throw PackageConfigArgumentError(
+ file.toString(), "file", "File cannot be read");
+ }
+ int firstChar = firstNonWhitespaceChar(bytes);
+ if (firstChar != $lbrace) {
+ // Definitely not a JSON object, probably a .packages.
+ var alternateFile = file.resolveUri(packageConfigJsonPath);
+ var alternateBytes = await loader(alternateFile);
+ if (alternateBytes == null) {
+ return packages_file.parse(bytes, file);
+ }
+ bytes = alternateBytes;
+ file = alternateFile;
+ }
+ return parsePackageConfigBytes(bytes, file);
+}
+
+Future<PackageConfig> readPackageConfigJsonFile(File file) async {
+ Uint8List bytes;
+ try {
+ bytes = await file.readAsBytes();
+ } catch (_) {
+ return null;
+ }
+ return parsePackageConfigBytes(bytes, file.uri);
+}
+
+Future<PackageConfig> readDotPackagesFile(File file) async {
+ Uint8List bytes;
+ try {
+ bytes = await file.readAsBytes();
+ } catch (_) {
+ return null;
+ }
+ return packages_file.parse(bytes, file.uri);
+}
+
+PackageConfig parsePackageConfigBytes(Uint8List bytes, Uri file) {
+ // TODO(lrn): Make this simpler. Maybe parse directly from bytes.
+ return parsePackageConfigJson(json.fuse(utf8).decode(bytes), file);
+}
+
+/// Creates a [PackageConfig] from a parsed JSON-like object structure.
+///
+/// The [json] argument must be a JSON object (`Map<String, dynamic>`)
+/// containing a `"configVersion"` entry with an integer value in the range
+/// 1 to [PackageConfig.maxVersion],
+/// and with a `"packages"` entry which is a JSON array (`List<dynamic>`)
+/// containing JSON objects which each has the following properties:
+///
+/// * `"name"`: The package name as a string.
+/// * `"rootUri"`: The root of the package as a URI stored as a string.
+/// * `"packageUri"`: Optionally the root of for `package:` URI resolution
+/// for the package, as a relative URI below the root URI
+/// stored as a string.
+/// * `"languageVersion"`: Optionally a language version string which is a
+/// an integer numeral, a decimal point (`.`) and another integer numeral,
+/// where the integer numeral cannot have a sign, and can only have a
+/// leading zero if the entire numeral is a single zero.
+///
+/// All other properties are stored in [extraData].
+///
+/// The [baseLocation] is used as base URI to resolve the "rootUri"
+/// URI referencestring.
+PackageConfig parsePackageConfigJson(dynamic json, Uri baseLocation) {
+ if (!baseLocation.hasScheme || baseLocation.isScheme("package")) {
+ throw PackageConfigArgumentError(baseLocation.toString(), "baseLocation",
+ "Must be an absolute non-package: URI");
+ }
+
+ if (!baseLocation.path.endsWith("/")) {
+ baseLocation = baseLocation.resolveUri(Uri(path: "."));
+ }
+
+ String typeName<T>() {
+ if (0 is T) return "int";
+ if ("" is T) return "string";
+ if (const [] is T) return "array";
+ return "object";
+ }
+
+ T checkType<T>(dynamic value, String name, [String /*?*/ packageName]) {
+ if (value is T) return value;
+ // The only types we are called with are [int], [String], [List<dynamic>]
+ // and Map<String, dynamic>. Recognize which to give a better error message.
+ var message =
+ "$name${packageName != null ? " of package $packageName" : ""}"
+ " is not a JSON ${typeName<T>()}";
+ throw PackageConfigFormatException(message, value);
+ }
+
+ Package parsePackage(Map<String, dynamic> entry) {
+ String /*?*/ name;
+ String /*?*/ rootUri;
+ String /*?*/ packageUri;
+ String /*?*/ languageVersion;
+ Map<String, dynamic> /*?*/ extraData;
+ entry.forEach((key, value) {
+ switch (key) {
+ case _nameKey:
+ name = checkType<String>(value, _nameKey);
+ break;
+ case _rootUriKey:
+ rootUri = checkType<String>(value, _rootUriKey, name);
+ break;
+ case _packageUriKey:
+ packageUri = checkType<String>(value, _packageUriKey, name);
+ break;
+ case _languageVersionKey:
+ languageVersion = checkType<String>(value, _languageVersionKey, name);
+ break;
+ default:
+ (extraData ??= {})[key] = value;
+ break;
+ }
+ });
+ if (name == null) {
+ throw PackageConfigFormatException("Missing name entry", entry);
+ }
+ if (rootUri == null) {
+ throw PackageConfigFormatException("Missing rootUri entry", entry);
+ }
+ Uri root = baseLocation.resolve(rootUri);
+ Uri /*?*/ packageRoot = root;
+ if (packageUri != null) packageRoot = root.resolve(packageUri);
+ try {
+ return SimplePackage(name, root, packageRoot, languageVersion, extraData);
+ } on ArgumentError catch (e) {
+ throw PackageConfigFormatException(e.message, e.invalidValue);
+ }
+ }
+
+ var map = checkType<Map<String, dynamic>>(json, "value");
+ Map<String, dynamic> /*?*/ extraData = null;
+ List<Package> /*?*/ packageList;
+ int /*?*/ configVersion;
+ map.forEach((key, value) {
+ switch (key) {
+ case _configVersionKey:
+ configVersion = checkType<int>(value, _configVersionKey);
+ break;
+ case _packagesKey:
+ var packageArray = checkType<List<dynamic>>(value, _packagesKey);
+ var packages = <Package>[];
+ for (var package in packageArray) {
+ packages.add(parsePackage(
+ checkType<Map<String, dynamic>>(package, "package entry")));
+ }
+ packageList = packages;
+ break;
+ default:
+ (extraData ??= {})[key] = value;
+ break;
+ }
+ });
+ if (configVersion == null) {
+ throw PackageConfigFormatException("Missing configVersion entry", json);
+ }
+ if (packageList == null)
+ throw PackageConfigFormatException("Missing packages list", json);
+ try {
+ return SimplePackageConfig(configVersion, packageList, extraData);
+ } on ArgumentError catch (e) {
+ throw PackageConfigFormatException(e.message, e.invalidValue);
+ }
+}
+
+Future<void> writePackageConfigJson(
+ PackageConfig config, Directory targetDirectory) async {
+ // Write .dart_tool/package_config.json first.
+ var file = File(
+ path.join(targetDirectory.path, ".dart_tool", "package_config.json"));
+ var baseUri = file.uri;
+ var extraData = config.extraData;
+ var data = <String, dynamic>{
+ _configVersionKey: PackageConfig.maxVersion,
+ _packagesKey: [
+ for (var package in config.packages)
+ <String, dynamic>{
+ _nameKey: package.name,
+ _rootUriKey: relativizeUri(package.root, baseUri),
+ if (package.root != package.packageUriRoot)
+ _packageUriKey: relativizeUri(package.packageUriRoot, package.root),
+ if (package.languageVersion != null)
+ _languageVersionKey: package.languageVersion,
+ ...?_extractExtraData(package.extraData, _packageNames),
+ }
+ ],
+ ...?_extractExtraData(config.extraData, _topNames),
+ };
+
+ // Write .packages too.
+ String /*?*/ comment;
+ if (extraData != null) {
+ String /*?*/ generator = extraData[_generatorKey];
+ if (generator != null) {
+ String /*?*/ generated = extraData[_generatedKey];
+ String /*?*/ generatorVersion = extraData[_generatorVersionKey];
+ comment = "Generated by $generator"
+ "${generatorVersion != null ? " $generatorVersion" : ""}"
+ "${generated != null ? " on $generated" : ""}.";
+ }
+ }
+ file = File(path.join(targetDirectory.path, ".packages"));
+ baseUri = file.uri;
+ var buffer = StringBuffer();
+ packages_file.write(buffer, config, baseUri: baseUri, comment: comment);
+
+ await Future.wait([
+ file.writeAsString(JsonEncoder.withIndent(" ").convert(data)),
+ file.writeAsString(buffer.toString()),
+ ]);
+}
+
+/// If "extraData" is a JSON map, then return it, otherwise return null.
+///
+/// If the value contains any of the [reservedNames] for the current context,
+/// entries with that name in the extra data are dropped.
+Map<String, dynamic> /*?*/ _extractExtraData(
+ dynamic data, Iterable<String> reservedNames) {
+ if (data is Map<String, dynamic>) {
+ if (data.isEmpty) return null;
+ for (var name in reservedNames) {
+ if (data.containsKey(name)) {
+ data = {
+ for (var key in data.keys)
+ if (!reservedNames.contains(key)) key: data[key]
+ };
+ if (data.isEmpty) return null;
+ for (var value in data.values) {
+ if (!_validateJson(value)) return null;
+ }
+ }
+ }
+ return data;
+ }
+ return null;
+}
+
+/// Checks that the object is a valid JSON-like data structure.
+bool _validateJson(dynamic object) {
+ if (object == null || object == true || object == false) return true;
+ if (object is num || object is String) return true;
+ if (object is List<dynamic>) {
+ for (var element in object) if (!_validateJson(element)) return false;
+ return true;
+ }
+ if (object is Map<String, dynamic>) {
+ for (var value in object.values) if (!_validateJson(value)) return false;
+ return true;
+ }
+ return false;
+}
diff --git a/lib/src/packages_file.dart b/lib/src/packages_file.dart
new file mode 100644
index 0000000..ac57b4f
--- /dev/null
+++ b/lib/src/packages_file.dart
@@ -0,0 +1,150 @@
+// 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 "package_config_impl.dart";
+import "package:charcode/ascii.dart";
+
+import "util.dart" show isValidPackageName, relativizeUri;
+import "errors.dart";
+
+/// Parses a `.packages` file into a [PackageConfig].
+///
+/// The [source] is the byte content of a `.packages` file, assumed to be
+/// UTF-8 encoded. In practice, all significant parts of the file must be ASCII,
+/// so Latin-1 or Windows-1252 encoding will also work fine.
+///
+/// If the file content is available as a string, its [String.codeUnits] can
+/// be used as the `source` argument of this function.
+///
+/// The [baseLocation] is used as a base URI to resolve all relative
+/// URI references against.
+/// If the content was read from a file, `baseLocation` should be the
+/// location of that file.
+///
+/// Returns a simple package configuration where each package's
+/// [Package.packageUriRoot] is the same as its [Package.root]
+/// and it has no [Package.languageVersion].
+PackageConfig parse(List<int> source, Uri baseLocation) {
+ if (baseLocation.isScheme("package")) {
+ throw PackageConfigArgumentError(
+ baseLocation, "baseLocation", "Must not be a package: URI");
+ }
+ int index = 0;
+ List<Package> packages = [];
+ Set<String> packageNames = {};
+ while (index < source.length) {
+ bool isComment = false;
+ int start = index;
+ int separatorIndex = -1;
+ int end = source.length;
+ int char = source[index++];
+ if (char == $cr || char == $lf) {
+ continue;
+ }
+ if (char == $colon) {
+ throw PackageConfigFormatException(
+ "Missing package name", source, index - 1);
+ }
+ isComment = char == $hash;
+ while (index < source.length) {
+ char = source[index++];
+ if (char == $colon && separatorIndex < 0) {
+ separatorIndex = index - 1;
+ } else if (char == $cr || char == $lf) {
+ end = index - 1;
+ break;
+ }
+ }
+ if (isComment) continue;
+ if (separatorIndex < 0) {
+ throw PackageConfigFormatException("No ':' on line", source, index - 1);
+ }
+ var packageName = String.fromCharCodes(source, start, separatorIndex);
+ if (!isValidPackageName(packageName)) {
+ throw PackageConfigFormatException(
+ "Not a valid package name", packageName, 0);
+ }
+ var packageValue = String.fromCharCodes(source, separatorIndex + 1, end);
+ Uri packageLocation = baseLocation.resolve(packageValue);
+ if (packageLocation.isScheme("package")) {
+ throw PackageConfigFormatException(
+ "Package URI as location for package", source, separatorIndex + 1);
+ }
+ if (packageLocation.hasQuery || packageLocation.hasFragment) {
+ throw PackageConfigFormatException(
+ "Location URI must not have query or fragment", source, start);
+ }
+ if (!packageLocation.path.endsWith('/')) {
+ packageLocation =
+ packageLocation.replace(path: packageLocation.path + "/");
+ }
+ if (packageNames.contains(packageName)) {
+ throw PackageConfigFormatException(
+ "Same package name occured more than once", source, start);
+ }
+ packages.add(SimplePackage(
+ packageName, packageLocation, packageLocation, null, null));
+ packageNames.add(packageName);
+ }
+ return SimplePackageConfig(1, packages, null);
+}
+
+/// Writes the configuration to a [StringSink].
+///
+/// If [comment] is provided, the output will contain this comment
+/// with `# ` in front of each line.
+/// Lines are defined as ending in line feed (`'\n'`). If the final
+/// line of the comment doesn't end in a line feed, one will be added.
+///
+/// If [baseUri] is provided, package locations will be made relative
+/// to the base URI, if possible, before writing.
+///
+/// If [allowDefaultPackage] is `true`, the [packageMapping] may contain an
+/// empty string mapping to the _default package name_.
+///
+/// All the keys of [packageMapping] must be valid package names,
+/// and the values must be URIs that do not have the `package:` scheme.
+void write(StringSink output, PackageConfig config,
+ {Uri baseUri, String comment}) {
+ if (baseUri != null && !baseUri.isAbsolute) {
+ throw PackageConfigArgumentError(baseUri, "baseUri", "Must be absolute");
+ }
+
+ if (comment != null) {
+ var lines = comment.split('\n');
+ if (lines.last.isEmpty) lines.removeLast();
+ for (var commentLine in lines) {
+ output.write('# ');
+ output.writeln(commentLine);
+ }
+ } else {
+ output.write("# generated by package:package_config at ");
+ output.write(DateTime.now());
+ output.writeln();
+ }
+ for (var package in config.packages) {
+ var packageName = package.name;
+ var uri = package.packageUriRoot;
+ // Validate packageName.
+ if (!isValidPackageName(packageName)) {
+ throw PackageConfigArgumentError(
+ config, "config", '"$packageName" is not a valid package name');
+ }
+ if (uri.scheme == "package") {
+ throw PackageConfigArgumentError(
+ config, "config", "Package location must not be a package URI: $uri");
+ }
+ output.write(packageName);
+ output.write(':');
+ // If baseUri provided, make uri relative.
+ if (baseUri != null) {
+ uri = relativizeUri(uri, baseUri);
+ }
+ if (!uri.path.endsWith('/')) {
+ uri = uri.replace(path: uri.path + '/');
+ }
+ output.write(uri);
+ output.writeln();
+ }
+}
diff --git a/lib/src/packages_impl.dart b/lib/src/packages_impl.dart
deleted file mode 100644
index 817002f..0000000
--- a/lib/src/packages_impl.dart
+++ /dev/null
@@ -1,127 +0,0 @@
-// Copyright (c) 2015, 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.
-
-/// Implementations of [Packages] that may be used in either server or browser
-/// based applications. For implementations that can only run in the browser,
-/// see [package_config.packages_io_impl].
-library package_config.packages_impl;
-
-import "dart:collection" show UnmodifiableMapView;
-
-import "../packages.dart";
-import "util.dart" show checkValidPackageUri;
-
-/// A [Packages] null-object.
-class NoPackages implements Packages {
- const NoPackages();
-
- Uri resolve(Uri packageUri, {Uri notFound(Uri packageUri)}) {
- String packageName = checkValidPackageUri(packageUri);
- if (notFound != null) return notFound(packageUri);
- throw new ArgumentError.value(
- packageUri, "packageUri", 'No package named "$packageName"');
- }
-
- Iterable<String> get packages => new Iterable<String>.empty();
-
- Map<String, Uri> asMap() => const <String, Uri>{};
-
- String get defaultPackageName => null;
-
- String packageMetadata(String packageName, String key) => null;
-
- String libraryMetadata(Uri libraryUri, String key) => null;
-}
-
-/// Base class for [Packages] implementations.
-///
-/// This class implements the [resolve] method in terms of a private
-/// member
-abstract class PackagesBase implements Packages {
- Uri resolve(Uri packageUri, {Uri notFound(Uri packageUri)}) {
- packageUri = packageUri.normalizePath();
- String packageName = checkValidPackageUri(packageUri);
- Uri packageBase = getBase(packageName);
- if (packageBase == null) {
- if (notFound != null) return notFound(packageUri);
- throw new ArgumentError.value(
- packageUri, "packageUri", 'No package named "$packageName"');
- }
- String packagePath = packageUri.path.substring(packageName.length + 1);
- return packageBase.resolve(packagePath);
- }
-
- /// Find a base location for a package name.
- ///
- /// Returns `null` if no package exists with that name, and that can be
- /// determined.
- Uri getBase(String packageName);
-
- String get defaultPackageName => null;
-
- String packageMetadata(String packageName, String key) => null;
-
- String libraryMetadata(Uri libraryUri, String key) => null;
-}
-
-/// A [Packages] implementation based on an existing map.
-class MapPackages extends PackagesBase {
- final Map<String, Uri> _mapping;
- MapPackages(this._mapping);
-
- Uri getBase(String packageName) =>
- packageName.isEmpty ? null : _mapping[packageName];
-
- Iterable<String> get packages => _mapping.keys;
-
- Map<String, Uri> asMap() => new UnmodifiableMapView<String, Uri>(_mapping);
-
- String get defaultPackageName => _mapping[""]?.toString();
-
- String packageMetadata(String packageName, String key) {
- if (packageName.isEmpty) return null;
- Uri uri = _mapping[packageName];
- if (uri == null || !uri.hasFragment) return null;
- // This can be optimized, either by caching the map or by
- // parsing incrementally instead of parsing the entire fragment.
- return Uri.splitQueryString(uri.fragment)[key];
- }
-
- String libraryMetadata(Uri libraryUri, String key) {
- if (libraryUri.isScheme("package")) {
- return packageMetadata(libraryUri.pathSegments.first, key);
- }
- var defaultPackageNameUri = _mapping[""];
- if (defaultPackageNameUri != null) {
- return packageMetadata(defaultPackageNameUri.toString(), key);
- }
- return null;
- }
-}
-
-/// A [Packages] implementation based on a remote (e.g., HTTP) directory.
-///
-/// There is no way to detect which packages exist short of trying to use
-/// them. You can't necessarily check whether a directory exists,
-/// except by checking for a know file in the directory.
-class NonFilePackagesDirectoryPackages extends PackagesBase {
- final Uri _packageBase;
- NonFilePackagesDirectoryPackages(this._packageBase);
-
- Uri getBase(String packageName) => _packageBase.resolve("$packageName/");
-
- Error _failListingPackages() {
- return new UnsupportedError(
- "Cannot list packages for a ${_packageBase.scheme}: "
- "based package root");
- }
-
- Iterable<String> get packages {
- throw _failListingPackages();
- }
-
- Map<String, Uri> asMap() {
- throw _failListingPackages();
- }
-}
diff --git a/lib/src/packages_io_impl.dart b/lib/src/packages_io_impl.dart
deleted file mode 100644
index 9eba9ce..0000000
--- a/lib/src/packages_io_impl.dart
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright (c) 2015, 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.
-
-/// Implementations of [Packages] that can only be used in server based
-/// applications.
-library package_config.packages_io_impl;
-
-import "dart:collection" show UnmodifiableMapView;
-import "dart:io" show Directory;
-
-import "package:path/path.dart" as path;
-
-import "packages_impl.dart";
-
-/// A [Packages] implementation based on a local directory.
-class FilePackagesDirectoryPackages extends PackagesBase {
- final Directory _packageDir;
- final Map<String, Uri> _packageToBaseUriMap = <String, Uri>{};
-
- FilePackagesDirectoryPackages(this._packageDir);
-
- Uri getBase(String packageName) {
- return _packageToBaseUriMap.putIfAbsent(packageName, () {
- return new Uri.file(path.join(_packageDir.path, packageName, '.'));
- });
- }
-
- Iterable<String> _listPackageNames() {
- return _packageDir
- .listSync()
- .where((e) => e is Directory)
- .map((e) => path.basename(e.path));
- }
-
- Iterable<String> get packages => _listPackageNames();
-
- Map<String, Uri> asMap() {
- var result = <String, Uri>{};
- for (var packageName in _listPackageNames()) {
- result[packageName] = getBase(packageName);
- }
- return new UnmodifiableMapView<String, Uri>(result);
- }
-}
diff --git a/lib/src/util.dart b/lib/src/util.dart
index f1e1afd..25d7b89 100644
--- a/lib/src/util.dart
+++ b/lib/src/util.dart
@@ -1,12 +1,17 @@
-// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
+// 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.
/// Utility methods used by more than one library in the package.
library package_config.util;
+import 'dart:io';
+import 'dart:typed_data';
+
import "package:charcode/ascii.dart";
+import "errors.dart";
+
// All ASCII characters that are valid in a package name, with space
// for all the invalid ones (including space).
const String _validPackageNameCharacters =
@@ -15,7 +20,7 @@
/// Tests whether something is a valid Dart package name.
bool isValidPackageName(String string) {
- return _findInvalidCharacter(string) < 0;
+ return checkPackageName(string) < 0;
}
/// Check if a string is a valid package name.
@@ -26,7 +31,7 @@
/// Returns `-1` if the string is valid.
/// Otherwise returns the index of the first invalid character,
/// or `string.length` if the string contains no non-'.' character.
-int _findInvalidCharacter(String string) {
+int checkPackageName(String string) {
// Becomes non-zero if any non-'.' character is encountered.
int nonDot = 0;
for (int i = 0; i < string.length; i++) {
@@ -40,47 +45,46 @@
return -1;
}
-/// Validate that a Uri is a valid package:URI.
-String checkValidPackageUri(Uri packageUri) {
+/// Validate that a [Uri] is a valid `package:` URI.
+String checkValidPackageUri(Uri packageUri, String name) {
if (packageUri.scheme != "package") {
- throw new ArgumentError.value(
- packageUri, "packageUri", "Not a package: URI");
+ throw PackageConfigArgumentError(packageUri, name, "Not a package: URI");
}
if (packageUri.hasAuthority) {
- throw new ArgumentError.value(
- packageUri, "packageUri", "Package URIs must not have a host part");
+ throw PackageConfigArgumentError(
+ packageUri, name, "Package URIs must not have a host part");
}
if (packageUri.hasQuery) {
// A query makes no sense if resolved to a file: URI.
- throw new ArgumentError.value(
- packageUri, "packageUri", "Package URIs must not have a query part");
+ throw PackageConfigArgumentError(
+ packageUri, name, "Package URIs must not have a query part");
}
if (packageUri.hasFragment) {
// We could leave the fragment after the URL when resolving,
// but it would be odd if "package:foo/foo.dart#1" and
// "package:foo/foo.dart#2" were considered different libraries.
// Keep the syntax open in case we ever get multiple libraries in one file.
- throw new ArgumentError.value(
- packageUri, "packageUri", "Package URIs must not have a fragment part");
+ throw PackageConfigArgumentError(
+ packageUri, name, "Package URIs must not have a fragment part");
}
if (packageUri.path.startsWith('/')) {
- throw new ArgumentError.value(
- packageUri, "packageUri", "Package URIs must not start with a '/'");
+ throw PackageConfigArgumentError(
+ packageUri, name, "Package URIs must not start with a '/'");
}
int firstSlash = packageUri.path.indexOf('/');
if (firstSlash == -1) {
- throw new ArgumentError.value(packageUri, "packageUri",
+ throw PackageConfigArgumentError(packageUri, name,
"Package URIs must start with the package name followed by a '/'");
}
String packageName = packageUri.path.substring(0, firstSlash);
- int badIndex = _findInvalidCharacter(packageName);
+ int badIndex = checkPackageName(packageName);
if (badIndex >= 0) {
if (packageName.isEmpty) {
- throw new ArgumentError.value(
- packageUri, "packageUri", "Package names mus be non-empty");
+ throw PackageConfigArgumentError(
+ packageUri, name, "Package names mus be non-empty");
}
if (badIndex == packageName.length) {
- throw new ArgumentError.value(packageUri, "packageUri",
+ throw PackageConfigArgumentError(packageUri, name,
"Package names must contain at least one non-'.' character");
}
assert(badIndex < packageName.length);
@@ -90,8 +94,211 @@
// Printable character.
badChar = "'${packageName[badIndex]}' ($badChar)";
}
- throw new ArgumentError.value(
- packageUri, "packageUri", "Package names must not contain $badChar");
+ throw PackageConfigArgumentError(
+ packageUri, name, "Package names must not contain $badChar");
}
return packageName;
}
+
+/// Checks whether [version] is a valid Dart language version string.
+///
+/// The format is (as RegExp) `^(0|[1-9]\d+)\.(0|[1-9]\d+)$`.
+///
+/// Returns the position of the first invalid character, or -1 if
+/// the string is valid.
+/// If the string is terminated early, the result is the length of the string.
+int checkValidVersionNumber(String version) {
+ if (version == null) {
+ return 0;
+ }
+ int index = 0;
+ int dotsSeen = 0;
+ outer:
+ for (;;) {
+ // Check for numeral.
+ if (index == version.length) return index;
+ int char = version.codeUnitAt(index++);
+ int digit = char ^ 0x30;
+ if (digit != 0) {
+ if (digit < 9) {
+ while (index < version.length) {
+ char = version.codeUnitAt(index++);
+ digit = char ^ 0x30;
+ if (digit < 9) continue;
+ if (char == 0x2e /*.*/) {
+ if (dotsSeen > 0) return index - 1;
+ dotsSeen = 1;
+ continue outer;
+ }
+ return index - 1;
+ }
+ if (dotsSeen > 0) return -1;
+ return index;
+ }
+ return index - 1;
+ }
+ // Leading zero means numeral is over.
+ if (index >= version.length) {
+ if (dotsSeen > 0) return -1;
+ return index;
+ }
+ if (dotsSeen > 0) return index;
+ char = version.codeUnitAt(index++);
+ if (char != 0x2e /*.*/) return index - 1;
+ }
+}
+
+/// Checks whether URI is just an absolute directory.
+///
+/// * It must have a scheme.
+/// * It must not have a query or fragment.
+/// * The path must start and end with `/`.
+bool isAbsoluteDirectoryUri(Uri uri) {
+ if (uri.hasQuery) return false;
+ if (uri.hasFragment) return false;
+ if (!uri.hasScheme) return false;
+ var path = uri.path;
+ if (!path.startsWith("/")) return false;
+ if (!path.endsWith("/")) return false;
+ return true;
+}
+
+/// Whether the former URI is a prefix of the latter.
+bool isUriPrefix(Uri prefix, Uri path) {
+ assert(!prefix.hasFragment);
+ assert(!prefix.hasQuery);
+ assert(!path.hasQuery);
+ assert(!path.hasFragment);
+ assert(prefix.path.endsWith('/'));
+ return path.toString().startsWith(prefix.toString());
+}
+
+/// Finds the first non-JSON-whitespace character in a file.
+///
+/// Used to heuristically detect whether a file is a JSON file or an .ini file.
+int firstNonWhitespaceChar(List<int> bytes) {
+ for (int i = 0; i < bytes.length; i++) {
+ var char = bytes[i];
+ if (char != 0x20 && char != 0x09 && char != 0x0a && char != 0x0d) {
+ return char;
+ }
+ }
+ return -1;
+}
+
+/// Attempts to return a relative path-only URI for [uri].
+///
+/// First removes any query or fragment part from [uri].
+///
+/// If [uri] is already relative (has no scheme), it's returned as-is.
+/// If that is not desired, the caller can pass `baseUri.resolveUri(uri)`
+/// as the [uri] instead.
+///
+/// If the [uri] has a scheme or authority part which differs from
+/// the [baseUri], or if there is no overlap in the paths of the
+/// two URIs at all, the [uri] is returned as-is.
+///
+/// Otherwise the result is a path-only URI which satsifies
+/// `baseUri.resolveUri(result) == uri`,
+///
+/// The `baseUri` must be absolute.
+Uri relativizeUri(Uri uri, Uri baseUri) {
+ assert(baseUri.isAbsolute);
+ if (uri.hasQuery || uri.hasFragment) {
+ uri = Uri(
+ scheme: uri.scheme,
+ userInfo: uri.hasAuthority ? uri.userInfo : null,
+ host: uri.hasAuthority ? uri.host : null,
+ port: uri.hasAuthority ? uri.port : null,
+ path: uri.path);
+ }
+
+ // Already relative. We assume the caller knows what they are doing.
+ if (!uri.isAbsolute) return uri;
+
+ if (baseUri.scheme != uri.scheme) {
+ return uri;
+ }
+
+ // If authority differs, we could remove the scheme, but it's not worth it.
+ if (uri.hasAuthority != baseUri.hasAuthority) return uri;
+ if (uri.hasAuthority) {
+ if (uri.userInfo != baseUri.userInfo ||
+ uri.host.toLowerCase() != baseUri.host.toLowerCase() ||
+ uri.port != baseUri.port) {
+ return uri;
+ }
+ }
+
+ baseUri = baseUri.normalizePath();
+ List<String> base = [...baseUri.pathSegments];
+ if (base.isNotEmpty) base.removeLast();
+ uri = uri.normalizePath();
+ List<String> target = [...uri.pathSegments];
+ if (target.isNotEmpty && target.last.isEmpty) target.removeLast();
+ int index = 0;
+ while (index < base.length && index < target.length) {
+ if (base[index] != target[index]) {
+ break;
+ }
+ index++;
+ }
+ if (index == base.length) {
+ if (index == target.length) {
+ return Uri(path: "./");
+ }
+ return Uri(path: target.skip(index).join('/'));
+ } else if (index > 0) {
+ var buffer = StringBuffer();
+ for (int n = base.length - index; n > 0; --n) {
+ buffer.write("../");
+ }
+ buffer.writeAll(target.skip(index), "/");
+ return Uri(path: buffer.toString());
+ } else {
+ return uri;
+ }
+}
+
+Future<Uint8List> defaultLoader(Uri uri) async {
+ if (uri.isScheme("file")) {
+ var file = File.fromUri(uri);
+ try {
+ return file.readAsBytes();
+ } catch (_) {
+ return null;
+ }
+ }
+ if (uri.isScheme("http") || uri.isScheme("https")) {
+ return _httpGet(uri);
+ }
+ throw UnsupportedError("Default URI unsupported scheme: $uri");
+}
+
+Future<Uint8List /*?*/ > _httpGet(Uri uri) async {
+ assert(uri.isScheme("http") || uri.isScheme("https"));
+ HttpClient client = new HttpClient();
+ HttpClientRequest request = await client.getUrl(uri);
+ HttpClientResponse response = await request.close();
+ if (response.statusCode != HttpStatus.ok) {
+ return null;
+ }
+ List<List<int>> splitContent = await response.toList();
+ int totalLength = 0;
+ if (splitContent.length == 1) {
+ var part = splitContent[0];
+ if (part is Uint8List) {
+ return part;
+ }
+ }
+ for (var list in splitContent) {
+ totalLength += list.length;
+ }
+ Uint8List result = new Uint8List(totalLength);
+ int offset = 0;
+ for (Uint8List contentPart in splitContent) {
+ result.setRange(offset, offset + contentPart.length, contentPart);
+ offset += contentPart.length;
+ }
+ return result;
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 72d299b..0cd7ddc 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,11 +1,11 @@
-name: package_config
-version: 1.2.0
-description: Support for working with Package Resolution config files.
+name: package_config_2
+version: 2.0.0
+description: Support for working with Package Configuration files.
author: Dart Team <misc@dartlang.org>
homepage: https://github.com/dart-lang/package_config
environment:
- sdk: '>=2.0.0-dev <3.0.0'
+ sdk: '>=2.5.0-dev <3.0.0'
dependencies:
charcode: ^1.1.0
diff --git a/test/all.dart b/test/all.dart
deleted file mode 100644
index 78e6cff..0000000
--- a/test/all.dart
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright (c) 2015, 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 "package:test/test.dart";
-
-import "discovery_analysis_test.dart" as discovery_analysis;
-import "discovery_test.dart" as discovery;
-import "parse_test.dart" as parse;
-import "parse_write_test.dart" as parse_write;
-
-main() {
- group("parse:", parse.main);
- group("discovery:", discovery.main);
- group("discovery-analysis:", discovery_analysis.main);
- group("parse/write:", parse_write.main);
-}
diff --git a/test/discovery_analysis_test.dart b/test/discovery_analysis_test.dart
deleted file mode 100644
index e432454..0000000
--- a/test/discovery_analysis_test.dart
+++ /dev/null
@@ -1,126 +0,0 @@
-// Copyright (c) 2015, 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.
-
-library package_config.discovery_analysis_test;
-
-import "dart:async";
-import "dart:io";
-
-import "package:package_config/discovery_analysis.dart";
-import "package:package_config/packages.dart";
-import "package:path/path.dart" as path;
-import "package:test/test.dart";
-
-main() {
- fileTest("basic", {
- ".packages": packagesFile,
- "foo": {".packages": packagesFile},
- "bar": {
- "packages": {"foo": {}, "bar": {}, "baz": {}}
- },
- "baz": {}
- }, (Directory directory) {
- var dirUri = new Uri.directory(directory.path);
- PackageContext ctx = PackageContext.findAll(directory);
- PackageContext root = ctx[directory];
- expect(root, same(ctx));
- validatePackagesFile(root.packages, dirUri);
- var fooDir = sub(directory, "foo");
- PackageContext foo = ctx[fooDir];
- expect(identical(root, foo), isFalse);
- validatePackagesFile(foo.packages, dirUri.resolve("foo/"));
- var barDir = sub(directory, "bar");
- PackageContext bar = ctx[sub(directory, "bar")];
- validatePackagesDir(bar.packages, dirUri.resolve("bar/"));
- PackageContext barbar = ctx[sub(barDir, "bar")];
- expect(barbar, same(bar)); // inherited.
- PackageContext baz = ctx[sub(directory, "baz")];
- expect(baz, same(root)); // inherited.
-
- var map = ctx.asMap();
- expect(map.keys.map((dir) => dir.path),
- unorderedEquals([directory.path, fooDir.path, barDir.path]));
- return null;
- });
-}
-
-Directory sub(Directory parent, String dirName) {
- return new Directory(path.join(parent.path, dirName));
-}
-
-const packagesFile = """
-# A comment
-foo:file:///dart/packages/foo/
-bar:http://example.com/dart/packages/bar/
-baz:packages/baz/
-""";
-
-void validatePackagesFile(Packages resolver, Uri location) {
- expect(resolver, isNotNull);
- expect(resolver.resolve(pkg("foo", "bar/baz")),
- equals(Uri.parse("file:///dart/packages/foo/bar/baz")));
- expect(resolver.resolve(pkg("bar", "baz/qux")),
- equals(Uri.parse("http://example.com/dart/packages/bar/baz/qux")));
- expect(resolver.resolve(pkg("baz", "qux/foo")),
- equals(location.resolve("packages/baz/qux/foo")));
- expect(resolver.packages, unorderedEquals(["foo", "bar", "baz"]));
-}
-
-void validatePackagesDir(Packages resolver, Uri location) {
- // Expect three packages: foo, bar and baz
- expect(resolver, isNotNull);
- expect(resolver.resolve(pkg("foo", "bar/baz")),
- equals(location.resolve("packages/foo/bar/baz")));
- expect(resolver.resolve(pkg("bar", "baz/qux")),
- equals(location.resolve("packages/bar/baz/qux")));
- expect(resolver.resolve(pkg("baz", "qux/foo")),
- equals(location.resolve("packages/baz/qux/foo")));
- if (location.scheme == "file") {
- expect(resolver.packages, unorderedEquals(["foo", "bar", "baz"]));
- } else {
- expect(() => resolver.packages, throwsUnsupportedError);
- }
-}
-
-Uri pkg(String packageName, String packagePath) {
- var path;
- if (packagePath.startsWith('/')) {
- path = "$packageName$packagePath";
- } else {
- path = "$packageName/$packagePath";
- }
- return new Uri(scheme: "package", path: path);
-}
-
-/// Create a directory structure from [description] and run [fileTest].
-///
-/// Description is a map, each key is a file entry. If the value is a map,
-/// it's a sub-dir, otherwise it's a file and the value is the content
-/// as a string.
-void fileTest(
- String name, Map description, Future fileTest(Directory directory)) {
- group("file-test", () {
- Directory tempDir = Directory.systemTemp.createTempSync("file-test");
- setUp(() {
- _createFiles(tempDir, description);
- });
- tearDown(() {
- tempDir.deleteSync(recursive: true);
- });
- test(name, () => fileTest(tempDir));
- });
-}
-
-void _createFiles(Directory target, Map description) {
- description.forEach((name, content) {
- if (content is Map) {
- Directory subDir = new Directory(path.join(target.path, name));
- subDir.createSync();
- _createFiles(subDir, content);
- } else {
- File file = new File(path.join(target.path, name));
- file.writeAsStringSync(content, flush: true);
- }
- });
-}
diff --git a/test/discovery_test.dart b/test/discovery_test.dart
index 2824272..5db24a1 100644
--- a/test/discovery_test.dart
+++ b/test/discovery_test.dart
@@ -1,327 +1,250 @@
-// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
+// 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.
library package_config.discovery_test;
-import "dart:async";
import "dart:io";
import "package:test/test.dart";
-import "package:package_config/packages.dart";
-import "package:package_config/discovery.dart";
-import "package:path/path.dart" as path;
+import "package:package_config_2/package_config.dart";
+
+import "src/util.dart";
const packagesFile = """
# A comment
foo:file:///dart/packages/foo/
-bar:http://example.com/dart/packages/bar/
+bar:/dart/packages/bar/
baz:packages/baz/
""";
-void validatePackagesFile(Packages resolver, Uri location) {
+const packageConfigFile = """
+{
+ "configVersion": 2,
+ "packages": [
+ {
+ "name": "foo",
+ "rootUri": "file:///dart/packages/foo/"
+ },
+ {
+ "name": "bar",
+ "rootUri": "/dart/packages/bar/"
+ },
+ {
+ "name": "baz",
+ "rootUri": "../packages/baz/"
+ }
+ ],
+ "extra": [42]
+}
+""";
+
+void validatePackagesFile(PackageConfig resolver, Directory directory) {
expect(resolver, isNotNull);
expect(resolver.resolve(pkg("foo", "bar/baz")),
equals(Uri.parse("file:///dart/packages/foo/bar/baz")));
expect(resolver.resolve(pkg("bar", "baz/qux")),
- equals(Uri.parse("http://example.com/dart/packages/bar/baz/qux")));
+ equals(Uri.parse("file:///dart/packages/bar/baz/qux")));
expect(resolver.resolve(pkg("baz", "qux/foo")),
- equals(location.resolve("packages/baz/qux/foo")));
- expect(resolver.packages, unorderedEquals(["foo", "bar", "baz"]));
-}
-
-void validatePackagesDir(Packages resolver, Uri location) {
- // Expect three packages: foo, bar and baz
- expect(resolver, isNotNull);
- expect(resolver.resolve(pkg("foo", "bar/baz")),
- equals(location.resolve("packages/foo/bar/baz")));
- expect(resolver.resolve(pkg("bar", "baz/qux")),
- equals(location.resolve("packages/bar/baz/qux")));
- expect(resolver.resolve(pkg("baz", "qux/foo")),
- equals(location.resolve("packages/baz/qux/foo")));
- if (location.scheme == "file") {
- expect(resolver.packages, unorderedEquals(["foo", "bar", "baz"]));
- } else {
- expect(() => resolver.packages, throwsUnsupportedError);
- }
-}
-
-Uri pkg(String packageName, String packagePath) {
- var path;
- if (packagePath.startsWith('/')) {
- path = "$packageName$packagePath";
- } else {
- path = "$packageName/$packagePath";
- }
- return new Uri(scheme: "package", path: path);
+ equals(Uri.directory(directory.path).resolve("packages/baz/qux/foo")));
+ expect([for (var p in resolver.packages) p.name],
+ unorderedEquals(["foo", "bar", "baz"]));
}
main() {
- generalTest(".packages", {
- ".packages": packagesFile,
- "script.dart": "main(){}",
- "packages": {"shouldNotBeFound": {}}
- }, (Uri location) async {
- Packages resolver;
- resolver = await findPackages(location);
- validatePackagesFile(resolver, location);
- resolver = await findPackages(location.resolve("script.dart"));
- validatePackagesFile(resolver, location);
- var specificDiscovery = (location.scheme == "file")
- ? findPackagesFromFile
- : findPackagesFromNonFile;
- resolver = await specificDiscovery(location);
- validatePackagesFile(resolver, location);
- resolver = await specificDiscovery(location.resolve("script.dart"));
- validatePackagesFile(resolver, location);
- });
-
- generalTest("packages/", {
- "packages": {"foo": {}, "bar": {}, "baz": {}},
- "script.dart": "main(){}"
- }, (Uri location) async {
- Packages resolver;
- bool isFile = (location.scheme == "file");
- resolver = await findPackages(location);
- validatePackagesDir(resolver, location);
- resolver = await findPackages(location.resolve("script.dart"));
- validatePackagesDir(resolver, location);
- var specificDiscovery =
- isFile ? findPackagesFromFile : findPackagesFromNonFile;
- resolver = await specificDiscovery(location);
- validatePackagesDir(resolver, location);
- resolver = await specificDiscovery(location.resolve("script.dart"));
- validatePackagesDir(resolver, location);
- });
-
- generalTest("underscore packages", {
- "packages": {"_foo": {}}
- }, (Uri location) async {
- Packages resolver = await findPackages(location);
- expect(resolver.resolve(pkg("_foo", "foo.dart")),
- equals(location.resolve("packages/_foo/foo.dart")));
- });
-
- fileTest(".packages recursive", {
- ".packages": packagesFile,
- "subdir": {"script.dart": "main(){}"}
- }, (Uri location) async {
- Packages resolver;
- resolver = await findPackages(location.resolve("subdir/"));
- validatePackagesFile(resolver, location);
- resolver = await findPackages(location.resolve("subdir/script.dart"));
- validatePackagesFile(resolver, location);
- resolver = await findPackagesFromFile(location.resolve("subdir/"));
- validatePackagesFile(resolver, location);
- resolver =
- await findPackagesFromFile(location.resolve("subdir/script.dart"));
- validatePackagesFile(resolver, location);
- });
-
- httpTest(".packages not recursive", {
- ".packages": packagesFile,
- "subdir": {"script.dart": "main(){}"}
- }, (Uri location) async {
- Packages resolver;
- var subdir = location.resolve("subdir/");
- resolver = await findPackages(subdir);
- validatePackagesDir(resolver, subdir);
- resolver = await findPackages(subdir.resolve("script.dart"));
- validatePackagesDir(resolver, subdir);
- resolver = await findPackagesFromNonFile(subdir);
- validatePackagesDir(resolver, subdir);
- resolver = await findPackagesFromNonFile(subdir.resolve("script.dart"));
- validatePackagesDir(resolver, subdir);
- });
-
- fileTest("no packages", {"script.dart": "main(){}"}, (Uri location) async {
- // A file: location with no .packages or packages returns
- // Packages.noPackages.
- Packages resolver;
- resolver = await findPackages(location);
- expect(resolver, same(Packages.noPackages));
- resolver = await findPackages(location.resolve("script.dart"));
- expect(resolver, same(Packages.noPackages));
- resolver = findPackagesFromFile(location);
- expect(resolver, same(Packages.noPackages));
- resolver = findPackagesFromFile(location.resolve("script.dart"));
- expect(resolver, same(Packages.noPackages));
- });
-
- httpTest("no packages", {"script.dart": "main(){}"}, (Uri location) async {
- // A non-file: location with no .packages or packages/:
- // Assumes a packages dir exists, and resolves relative to that.
- Packages resolver;
- resolver = await findPackages(location);
- validatePackagesDir(resolver, location);
- resolver = await findPackages(location.resolve("script.dart"));
- validatePackagesDir(resolver, location);
- resolver = await findPackagesFromNonFile(location);
- validatePackagesDir(resolver, location);
- resolver = await findPackagesFromNonFile(location.resolve("script.dart"));
- validatePackagesDir(resolver, location);
- });
-
- test(".packages w/ loader", () async {
- Uri location = Uri.parse("krutch://example.com/path/");
- Future<List<int>> loader(Uri file) async {
- if (file.path.endsWith(".packages")) {
- return packagesFile.codeUnits;
+ group("findPackages", () {
+ // Finds package_config.json if there.
+ fileTest("package_config.json", {
+ ".packages": "invalid .packages file",
+ "script.dart": "main(){}",
+ "packages": {"shouldNotBeFound": {}},
+ ".dart_tool": {
+ "package_config.json": packageConfigFile,
}
- throw "not found";
- }
-
- // A non-file: location with no .packages or packages/:
- // Assumes a packages dir exists, and resolves relative to that.
- Packages resolver;
- resolver = await findPackages(location, loader: loader);
- validatePackagesFile(resolver, location);
- resolver =
- await findPackages(location.resolve("script.dart"), loader: loader);
- validatePackagesFile(resolver, location);
- resolver = await findPackagesFromNonFile(location, loader: loader);
- validatePackagesFile(resolver, location);
- resolver = await findPackagesFromNonFile(location.resolve("script.dart"),
- loader: loader);
- validatePackagesFile(resolver, location);
- });
-
- test("no packages w/ loader", () async {
- Uri location = Uri.parse("krutch://example.com/path/");
- Future<List<int>> loader(Uri file) async {
- throw "not found";
- }
-
- // A non-file: location with no .packages or packages/:
- // Assumes a packages dir exists, and resolves relative to that.
- Packages resolver;
- resolver = await findPackages(location, loader: loader);
- validatePackagesDir(resolver, location);
- resolver =
- await findPackages(location.resolve("script.dart"), loader: loader);
- validatePackagesDir(resolver, location);
- resolver = await findPackagesFromNonFile(location, loader: loader);
- validatePackagesDir(resolver, location);
- resolver = await findPackagesFromNonFile(location.resolve("script.dart"),
- loader: loader);
- validatePackagesDir(resolver, location);
- });
-
- generalTest("loadPackagesFile", {".packages": packagesFile},
- (Uri directory) async {
- Uri file = directory.resolve(".packages");
- Packages resolver = await loadPackagesFile(file);
- validatePackagesFile(resolver, file);
- });
-
- generalTest(
- "loadPackagesFile non-default name", {"pheldagriff": packagesFile},
- (Uri directory) async {
- Uri file = directory.resolve("pheldagriff");
- Packages resolver = await loadPackagesFile(file);
- validatePackagesFile(resolver, file);
- });
-
- test("loadPackagesFile w/ loader", () async {
- Future<List<int>> loader(Uri uri) async => packagesFile.codeUnits;
- Uri file = Uri.parse("krutz://example.com/.packages");
- Packages resolver = await loadPackagesFile(file, loader: loader);
- validatePackagesFile(resolver, file);
- });
-
- generalTest("loadPackagesFile not found", {}, (Uri directory) async {
- Uri file = directory.resolve(".packages");
- expect(
- loadPackagesFile(file),
- throwsA(anyOf(new TypeMatcher<FileSystemException>(),
- new TypeMatcher<HttpException>())));
- });
-
- generalTest("loadPackagesFile syntax error", {".packages": "syntax error"},
- (Uri directory) async {
- Uri file = directory.resolve(".packages");
- expect(loadPackagesFile(file), throwsFormatException);
- });
-
- generalTest("getPackagesDir", {
- "packages": {"foo": {}, "bar": {}, "baz": {}}
- }, (Uri directory) async {
- Uri packages = directory.resolve("packages/");
- Packages resolver = getPackagesDirectory(packages);
- Uri resolved = resolver.resolve(pkg("foo", "flip/flop"));
- expect(resolved, packages.resolve("foo/flip/flop"));
- });
-}
-
-/// Create a directory structure from [description] and run [fileTest].
-///
-/// Description is a map, each key is a file entry. If the value is a map,
-/// it's a sub-dir, otherwise it's a file and the value is the content
-/// as a string.
-void fileTest(String name, Map description, Future fileTest(Uri directory)) {
- group("file-test", () {
- Directory tempDir = Directory.systemTemp.createTempSync("file-test");
- setUp(() {
- _createFiles(tempDir, description);
+ }, (Directory directory) async {
+ PackageConfig config = await findPackageConfig(directory);
+ expect(config.version, 2); // Found package_config.json file.
+ validatePackagesFile(config, directory);
});
- tearDown(() {
- tempDir.deleteSync(recursive: true);
- });
- test(name, () => fileTest(new Uri.file(path.join(tempDir.path, "."))));
- });
-}
-/// HTTP-server the directory structure from [description] and run [htpTest].
-///
-/// Description is a map, each key is a file entry. If the value is a map,
-/// it's a sub-dir, otherwise it's a file and the value is the content
-/// as a string.
-void httpTest(String name, Map description, Future httpTest(Uri directory)) {
- group("http-test", () {
- var serverSub;
- var uri;
- setUp(() {
- return HttpServer.bind(InternetAddress.loopbackIPv4, 0).then((server) {
- uri = new Uri(
- scheme: "http", host: "127.0.0.1", port: server.port, path: "/");
- serverSub = server.listen((HttpRequest request) {
- // No error handling.
- var path = request.uri.path;
- if (path.startsWith('/')) path = path.substring(1);
- if (path.endsWith('/')) path = path.substring(0, path.length - 1);
- var parts = path.split('/');
- dynamic fileOrDir = description;
- for (int i = 0; i < parts.length; i++) {
- fileOrDir = fileOrDir[parts[i]];
- if (fileOrDir == null) {
- request.response.statusCode = 404;
- request.response.close();
- return;
- }
- }
- request.response.write(fileOrDir);
- request.response.close();
- });
+ // Finds .packages if no package_config.json.
+ fileTest(".packages", {
+ ".packages": packagesFile,
+ "script.dart": "main(){}",
+ "packages": {"shouldNotBeFound": {}}
+ }, (Directory directory) async {
+ PackageConfig config = await findPackageConfig(directory);
+ expect(config.version, 1); // Found .packages file.
+ validatePackagesFile(config, directory);
+ });
+
+ // Finds package_config.json in super-directory.
+ fileTest("package_config.json recursive", {
+ ".packages": packagesFile,
+ ".dart_tool": {
+ "package_config.json": packageConfigFile,
+ },
+ "subdir": {
+ "script.dart": "main(){}",
+ }
+ }, (Directory directory) async {
+ PackageConfig config =
+ await findPackageConfig(subdir(directory, "subdir/"));
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+
+ // Finds .packages in super-directory.
+ fileTest(".packages recursive", {
+ ".packages": packagesFile,
+ "subdir": {"script.dart": "main(){}"}
+ }, (Directory directory) async {
+ PackageConfig config;
+ config = await findPackageConfig(subdir(directory, "subdir/"));
+ expect(config.version, 1);
+ validatePackagesFile(config, directory);
+ });
+
+ // Does not find a packages/ directory, and returns null if nothing found.
+ fileTest("package directory packages not supported", {
+ "packages": {
+ "foo": {},
+ }
+ }, (Directory directory) async {
+ PackageConfig config = await findPackageConfig(directory);
+ expect(config, null);
+ });
+
+ fileTest("invalid .packages", {
+ ".packages": "not a .packages file",
+ }, (Directory directory) {
+ expect(() => findPackageConfig(directory),
+ throwsA(TypeMatcher<FormatException>()));
+ });
+
+ fileTest("invalid .packages as JSON", {
+ ".packages": packageConfigFile,
+ }, (Directory directory) {
+ expect(() => findPackageConfig(directory),
+ throwsA(TypeMatcher<FormatException>()));
+ });
+
+ fileTest("invalid .packages", {
+ ".dart_tool": {
+ "package_config.json": "not a JSON file",
+ }
+ }, (Directory directory) {
+ expect(() => findPackageConfig(directory),
+ throwsA(TypeMatcher<FormatException>()));
+ });
+
+ fileTest("invalid .packages as INI", {
+ ".dart_tool": {
+ "package_config.json": packagesFile,
+ }
+ }, (Directory directory) {
+ expect(() => findPackageConfig(directory),
+ throwsA(TypeMatcher<FormatException>()));
+ });
+ });
+
+ group("loadPackageConfig", () {
+ // Load a specific files
+ group("package_config.json", () {
+ var files = {
+ ".packages": packagesFile,
+ ".dart_tool": {
+ "package_config.json": packageConfigFile,
+ },
+ };
+ fileTest("directly", files, (Directory directory) async {
+ File file =
+ dirFile(subdir(directory, ".dart_tool"), "package_config.json");
+ PackageConfig config = await loadPackageConfig(file);
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+ fileTest("indirectly through .packages", files,
+ (Directory directory) async {
+ File file = dirFile(directory, ".packages");
+ PackageConfig config = await loadPackageConfig(file);
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
});
});
- tearDown(() => serverSub.cancel());
- test(name, () => httpTest(uri));
- });
-}
-void generalTest(String name, Map description, Future action(Uri location)) {
- fileTest(name, description, action);
- httpTest(name, description, action);
-}
+ fileTest("package_config.json non-default name", {
+ ".packages": packagesFile,
+ "subdir": {
+ "pheldagriff": packageConfigFile,
+ },
+ }, (Directory directory) async {
+ File file = dirFile(directory, "subdir/pheldagriff");
+ PackageConfig config = await loadPackageConfig(file);
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
-void _createFiles(Directory target, Map description) {
- description.forEach((name, content) {
- if (content is Map) {
- Directory subDir = new Directory(path.join(target.path, name));
- subDir.createSync();
- _createFiles(subDir, content);
- } else {
- File file = new File(path.join(target.path, name));
- file.writeAsStringSync(content, flush: true);
- }
+ fileTest("package_config.json named .packages", {
+ "subdir": {
+ ".packages": packageConfigFile,
+ },
+ }, (Directory directory) async {
+ File file = dirFile(directory, "subdir/.packages");
+ PackageConfig config = await loadPackageConfig(file);
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+
+ fileTest(".packages", {
+ ".packages": packagesFile,
+ }, (Directory directory) async {
+ File file = dirFile(directory, ".packages");
+ PackageConfig config = await loadPackageConfig(file);
+ expect(config.version, 1);
+ validatePackagesFile(config, directory);
+ });
+
+ fileTest(".packages non-default name", {
+ "pheldagriff": packagesFile,
+ }, (Directory directory) async {
+ File file = dirFile(directory, "pheldagriff");
+ PackageConfig config = await loadPackageConfig(file);
+ expect(config.version, 1);
+ validatePackagesFile(config, directory);
+ });
+
+ fileTest("no config found", {}, (Directory directory) {
+ File file = dirFile(directory, "anyname");
+ expect(() => loadPackageConfig(file),
+ throwsA(TypeMatcher<FileSystemException>()));
+ });
+
+ fileTest("specified file syntax error", {
+ "anyname": "syntax error",
+ }, (Directory directory) {
+ File file = dirFile(directory, "anyname");
+ expect(() => loadPackageConfig(file), throwsFormatException);
+ });
+
+ // Find package_config.json in subdir even if initial file syntax error.
+ fileTest("specified file syntax error", {
+ "anyname": "syntax error",
+ ".dart_tool": {
+ "package_config.json": packageConfigFile,
+ },
+ }, (Directory directory) async {
+ File file = dirFile(directory, "anyname");
+ PackageConfig config = await loadPackageConfig(file);
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+
+ // A file starting with `{` is a package_config.json file.
+ fileTest("file syntax error with {", {
+ ".packages": "{syntax error",
+ }, (Directory directory) {
+ File file = dirFile(directory, ".packages");
+ expect(() => loadPackageConfig(file), throwsFormatException);
+ });
});
}
diff --git a/test/discovery_uri_test.dart b/test/discovery_uri_test.dart
new file mode 100644
index 0000000..414a43a
--- /dev/null
+++ b/test/discovery_uri_test.dart
@@ -0,0 +1,256 @@
+// 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.
+
+library package_config.discovery_test;
+
+import "dart:io";
+import "package:test/test.dart";
+import "package:package_config_2/package_config.dart";
+
+import "src/util.dart";
+
+const packagesFile = """
+# A comment
+foo:file:///dart/packages/foo/
+bar:/dart/packages/bar/
+baz:packages/baz/
+""";
+
+const packageConfigFile = """
+{
+ "configVersion": 2,
+ "packages": [
+ {
+ "name": "foo",
+ "rootUri": "file:///dart/packages/foo/"
+ },
+ {
+ "name": "bar",
+ "rootUri": "/dart/packages/bar/"
+ },
+ {
+ "name": "baz",
+ "rootUri": "../packages/baz/"
+ }
+ ],
+ "extra": [42]
+}
+""";
+
+void validatePackagesFile(PackageConfig resolver, Uri directory) {
+ expect(resolver, isNotNull);
+ expect(resolver.resolve(pkg("foo", "bar/baz")),
+ equals(Uri.parse("file:///dart/packages/foo/bar/baz")));
+ expect(resolver.resolve(pkg("bar", "baz/qux")),
+ equals(directory.resolve("/dart/packages/bar/baz/qux")));
+ expect(resolver.resolve(pkg("baz", "qux/foo")),
+ equals(directory.resolve("packages/baz/qux/foo")));
+ expect([for (var p in resolver.packages) p.name],
+ unorderedEquals(["foo", "bar", "baz"]));
+}
+
+main() {
+ group("findPackages", () {
+ // Finds package_config.json if there.
+ loaderTest("package_config.json", {
+ ".packages": "invalid .packages file",
+ "script.dart": "main(){}",
+ "packages": {"shouldNotBeFound": {}},
+ ".dart_tool": {
+ "package_config.json": packageConfigFile,
+ }
+ }, (Uri directory, loader) async {
+ PackageConfig config =
+ await findPackageConfigUri(directory, loader: loader);
+ expect(config.version, 2); // Found package_config.json file.
+ validatePackagesFile(config, directory);
+ });
+
+ // Finds .packages if no package_config.json.
+ loaderTest(".packages", {
+ ".packages": packagesFile,
+ "script.dart": "main(){}",
+ "packages": {"shouldNotBeFound": {}}
+ }, (Uri directory, loader) async {
+ PackageConfig config =
+ await findPackageConfigUri(directory, loader: loader);
+ expect(config.version, 1); // Found .packages file.
+ validatePackagesFile(config, directory);
+ });
+
+ // Finds package_config.json in super-directory.
+ loaderTest("package_config.json recursive", {
+ ".packages": packagesFile,
+ ".dart_tool": {
+ "package_config.json": packageConfigFile,
+ },
+ "subdir": {
+ "script.dart": "main(){}",
+ }
+ }, (Uri directory, loader) async {
+ PackageConfig config = await findPackageConfigUri(
+ directory.resolve("subdir/"),
+ loader: loader);
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+
+ // Finds .packages in super-directory.
+ loaderTest(".packages recursive", {
+ ".packages": packagesFile,
+ "subdir": {"script.dart": "main(){}"}
+ }, (Uri directory, loader) async {
+ PackageConfig config;
+ config = await findPackageConfigUri(directory.resolve("subdir/"),
+ loader: loader);
+ expect(config.version, 1);
+ validatePackagesFile(config, directory);
+ });
+
+ // Does not find a packages/ directory, and returns null if nothing found.
+ loaderTest("package directory packages not supported", {
+ "packages": {
+ "foo": {},
+ }
+ }, (Uri directory, loader) async {
+ PackageConfig config =
+ await findPackageConfigUri(directory, loader: loader);
+ expect(config, null);
+ });
+
+ loaderTest("invalid .packages", {
+ ".packages": "not a .packages file",
+ }, (Uri directory, loader) {
+ expect(() => findPackageConfigUri(directory, loader: loader),
+ throwsA(TypeMatcher<FormatException>()));
+ });
+
+ loaderTest("invalid .packages as JSON", {
+ ".packages": packageConfigFile,
+ }, (Uri directory, loader) {
+ expect(() => findPackageConfigUri(directory, loader: loader),
+ throwsA(TypeMatcher<FormatException>()));
+ });
+
+ loaderTest("invalid .packages", {
+ ".dart_tool": {
+ "package_config.json": "not a JSON file",
+ }
+ }, (Uri directory, loader) {
+ expect(() => findPackageConfigUri(directory, loader: loader),
+ throwsA(TypeMatcher<FormatException>()));
+ });
+
+ loaderTest("invalid .packages as INI", {
+ ".dart_tool": {
+ "package_config.json": packagesFile,
+ }
+ }, (Uri directory, loader) {
+ expect(() => findPackageConfigUri(directory, loader: loader),
+ throwsA(TypeMatcher<FormatException>()));
+ });
+ });
+
+ group("loadPackageConfig", () {
+ // Load a specific files
+ group("package_config.json", () {
+ var files = {
+ ".packages": packagesFile,
+ ".dart_tool": {
+ "package_config.json": packageConfigFile,
+ },
+ };
+ loaderTest("directly", files, (Uri directory, loader) async {
+ Uri file = directory.resolve(".dart_tool/package_config.json");
+ PackageConfig config = await loadPackageConfigUri(file, loader: loader);
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+ loaderTest("indirectly through .packages", files,
+ (Uri directory, loader) async {
+ Uri file = directory.resolve(".packages");
+ PackageConfig config = await loadPackageConfigUri(file, loader: loader);
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+ });
+
+ loaderTest("package_config.json non-default name", {
+ ".packages": packagesFile,
+ "subdir": {
+ "pheldagriff": packageConfigFile,
+ },
+ }, (Uri directory, loader) async {
+ Uri file = directory.resolve("subdir/pheldagriff");
+ PackageConfig config = await loadPackageConfigUri(file, loader: loader);
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+
+ loaderTest("package_config.json named .packages", {
+ "subdir": {
+ ".packages": packageConfigFile,
+ },
+ }, (Uri directory, loader) async {
+ Uri file = directory.resolve("subdir/.packages");
+ PackageConfig config = await loadPackageConfigUri(file, loader: loader);
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+
+ loaderTest(".packages", {
+ ".packages": packagesFile,
+ }, (Uri directory, loader) async {
+ Uri file = directory.resolve(".packages");
+ PackageConfig config = await loadPackageConfigUri(file, loader: loader);
+ expect(config.version, 1);
+ validatePackagesFile(config, directory);
+ });
+
+ loaderTest(".packages non-default name", {
+ "pheldagriff": packagesFile,
+ }, (Uri directory, loader) async {
+ Uri file = directory.resolve("pheldagriff");
+ PackageConfig config = await loadPackageConfigUri(file, loader: loader);
+ expect(config.version, 1);
+ validatePackagesFile(config, directory);
+ });
+
+ loaderTest("no config found", {}, (Uri directory, loader) {
+ Uri file = directory.resolve("anyname");
+ expect(() => loadPackageConfigUri(file, loader: loader),
+ throwsArgumentError);
+ });
+
+ loaderTest("specified file syntax error", {
+ "anyname": "syntax error",
+ }, (Uri directory, loader) {
+ Uri file = directory.resolve("anyname");
+ expect(() => loadPackageConfigUri(file, loader: loader),
+ throwsFormatException);
+ });
+
+ // Find package_config.json in subdir even if initial file syntax error.
+ loaderTest("specified file syntax error", {
+ "anyname": "syntax error",
+ ".dart_tool": {
+ "package_config.json": packageConfigFile,
+ },
+ }, (Uri directory, loader) async {
+ Uri file = directory.resolve("anyname");
+ PackageConfig config = await loadPackageConfigUri(file, loader: loader);
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+
+ // A file starting with `{` is a package_config.json file.
+ loaderTest("file syntax error with {", {
+ ".packages": "{syntax error",
+ }, (Uri directory, loader) {
+ Uri file = directory.resolve(".packages");
+ expect(() => loadPackageConfigUri(file, loader: loader),
+ throwsFormatException);
+ });
+ });
+}
diff --git a/test/parse_test.dart b/test/parse_test.dart
index b9b1bb5..235f493 100644
--- a/test/parse_test.dart
+++ b/test/parse_test.dart
@@ -1,245 +1,312 @@
-// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
+// 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.
-library package_config.parse_test;
+import "dart:io";
+import "dart:convert";
-import "package:package_config/packages.dart";
-import "package:package_config/packages_file.dart" show parse;
-import "package:package_config/src/packages_impl.dart";
import "package:test/test.dart";
-main() {
- var base = Uri.parse("file:///one/two/three/packages.map");
- test("empty", () {
- var packages = doParse(emptySample, base);
- expect(packages.asMap(), isEmpty);
- });
- test("comment only", () {
- var packages = doParse(commentOnlySample, base);
- expect(packages.asMap(), isEmpty);
- });
- test("empty lines only", () {
- var packages = doParse(emptyLinesSample, base);
- expect(packages.asMap(), isEmpty);
- });
+import "package:package_config_2/src/packages_file.dart" as packages;
+import "package:package_config_2/src/package_config_json.dart";
+import "src/util.dart";
- test("empty lines only", () {
- var packages = doParse(emptyLinesSample, base);
- expect(packages.asMap(), isEmpty);
- });
+void main() {
+ group(".packages", () {
+ test("valid", () {
+ var packagesFile = "# Generated by pub yadda yadda\n"
+ "foo:file:///foo/lib/\n"
+ "bar:/bar/lib/\n"
+ "baz:lib/\n";
+ var result = packages.parse(
+ utf8.encode(packagesFile), Uri.parse("file:///tmp/file.dart"));
+ expect(result.version, 1);
+ expect({for (var p in result.packages) p.name}, {"foo", "bar", "baz"});
+ expect(result.resolve(pkg("foo", "foo.dart")),
+ Uri.parse("file:///foo/lib/foo.dart"));
+ expect(result.resolve(pkg("bar", "bar.dart")),
+ Uri.parse("file:///bar/lib/bar.dart"));
+ expect(result.resolve(pkg("baz", "baz.dart")),
+ Uri.parse("file:///tmp/lib/baz.dart"));
- test("single", () {
- var packages = doParse(singleRelativeSample, base);
- expect(packages.packages.toList(), equals(["foo"]));
- expect(packages.resolve(Uri.parse("package:foo/bar/baz.dart")),
- equals(base.resolve("../test/").resolve("bar/baz.dart")));
- });
-
- test("single no slash", () {
- var packages = doParse(singleRelativeSampleNoSlash, base);
- expect(packages.packages.toList(), equals(["foo"]));
- expect(packages.resolve(Uri.parse("package:foo/bar/baz.dart")),
- equals(base.resolve("../test/").resolve("bar/baz.dart")));
- });
-
- test("single no newline", () {
- var packages = doParse(singleRelativeSampleNoNewline, base);
- expect(packages.packages.toList(), equals(["foo"]));
- expect(packages.resolve(Uri.parse("package:foo/bar/baz.dart")),
- equals(base.resolve("../test/").resolve("bar/baz.dart")));
- });
-
- test("single absolute authority", () {
- var packages = doParse(singleAbsoluteSample, base);
- expect(packages.packages.toList(), equals(["foo"]));
- expect(packages.resolve(Uri.parse("package:foo/bar/baz.dart")),
- equals(Uri.parse("http://example.com/some/where/bar/baz.dart")));
- });
-
- test("single empty path", () {
- var packages = doParse(singleEmptyPathSample, base);
- expect(packages.packages.toList(), equals(["foo"]));
- expect(packages.resolve(Uri.parse("package:foo/bar/baz.dart")),
- equals(base.replace(path: "${base.path}/bar/baz.dart")));
- });
-
- test("single absolute path", () {
- var packages = doParse(singleAbsolutePathSample, base);
- expect(packages.packages.toList(), equals(["foo"]));
- expect(packages.resolve(Uri.parse("package:foo/bar/baz.dart")),
- equals(base.replace(path: "/test/bar/baz.dart")));
- });
-
- test("multiple", () {
- var packages = doParse(multiRelativeSample, base);
- expect(packages.packages.toList()..sort(), equals(["bar", "foo"]));
- expect(packages.resolve(Uri.parse("package:foo/bar/baz.dart")),
- equals(base.resolve("../test/").resolve("bar/baz.dart")));
- expect(packages.resolve(Uri.parse("package:bar/foo/baz.dart")),
- equals(base.resolve("../test2/").resolve("foo/baz.dart")));
- });
-
- test("dot-dot 1", () {
- var packages = doParse(singleRelativeSample, base);
- expect(packages.packages.toList(), equals(["foo"]));
- expect(packages.resolve(Uri.parse("package:foo/qux/../bar/baz.dart")),
- equals(base.resolve("../test/").resolve("bar/baz.dart")));
- });
-
- test("all valid chars can be used in URI segment", () {
- var packages = doParse(allValidCharsSample, base);
- expect(packages.packages.toList(), equals([allValidChars]));
- expect(packages.resolve(Uri.parse("package:$allValidChars/bar/baz.dart")),
- equals(base.resolve("../test/").resolve("bar/baz.dart")));
- });
-
- test("no invalid chars accepted", () {
- var map = {};
- for (int i = 0; i < allValidChars.length; i++) {
- map[allValidChars.codeUnitAt(i)] = true;
- }
- for (int i = 0; i <= 255; i++) {
- if (map[i] == true) continue;
- var char = new String.fromCharCode(i);
- expect(() => doParse("x${char}x:x", null),
- anyOf(throwsNoSuchMethodError, throwsFormatException));
- }
- });
-
- test("no escapes", () {
- expect(() => doParse("x%41x:x", base), throwsFormatException);
- });
-
- test("same name twice", () {
- expect(
- () => doParse(singleRelativeSample * 2, base), throwsFormatException);
- });
-
- test("disallow default package", () {
- expect(() => doParse(":foo", base, allowDefaultPackage: false),
- throwsFormatException);
- });
-
- test("allow default package", () {
- var packages = doParse(":foo", base, allowDefaultPackage: true);
- expect(packages.defaultPackageName, "foo");
- });
-
- test("allow default package name with dot", () {
- var packages = doParse(":foo.bar", base, allowDefaultPackage: true);
- expect(packages.defaultPackageName, "foo.bar");
- });
-
- test("not two default packages", () {
- expect(() => doParse(":foo\n:bar", base, allowDefaultPackage: true),
- throwsFormatException);
- });
-
- test("default package invalid package name", () {
- // Not a valid *package name*.
- expect(() => doParse(":foo/bar", base, allowDefaultPackage: true),
- throwsFormatException);
- });
-
- group("metadata", () {
- var packages = doParse(
- ":foo\n"
- "foo:foo#metafoo=1\n"
- "bar:bar#metabar=2\n"
- "baz:baz\n"
- "qux:qux#metaqux1=3&metaqux2=4\n",
- base,
- allowDefaultPackage: true);
- test("non-existing", () {
- // non-package name.
- expect(packages.packageMetadata("///", "f"), null);
- expect(packages.packageMetadata("", "f"), null);
- // unconfigured package name.
- expect(packages.packageMetadata("absent", "f"), null);
- // package name without that metadata
- expect(packages.packageMetadata("foo", "notfoo"), null);
+ var foo = result["foo"];
+ expect(foo, isNotNull);
+ expect(foo.root, Uri.parse("file:///foo/lib/"));
+ expect(foo.packageUriRoot, Uri.parse("file:///foo/lib/"));
+ expect(foo.languageVersion, null);
});
- test("lookup", () {
- expect(packages.packageMetadata("foo", "metafoo"), "1");
- expect(packages.packageMetadata("bar", "metabar"), "2");
- expect(packages.packageMetadata("qux", "metaqux1"), "3");
- expect(packages.packageMetadata("qux", "metaqux2"), "4");
- });
- test("by library URI", () {
- expect(
- packages.libraryMetadata(
- Uri.parse("package:foo/index.dart"), "metafoo"),
- "1");
- expect(
- packages.libraryMetadata(
- Uri.parse("package:bar/index.dart"), "metabar"),
- "2");
- expect(
- packages.libraryMetadata(
- Uri.parse("package:qux/index.dart"), "metaqux1"),
- "3");
- expect(
- packages.libraryMetadata(
- Uri.parse("package:qux/index.dart"), "metaqux2"),
- "4");
- });
- test("by default package", () {
- expect(
- packages.libraryMetadata(
- Uri.parse("file:///whatever.dart"), "metafoo"),
- "1");
- });
- });
- for (String invalidSample in invalid) {
- test("invalid '$invalidSample'", () {
- var result;
- try {
- result = doParse(invalidSample, base);
- } on FormatException {
- // expected
- return;
+ test("valid empty", () {
+ var packagesFile = "# Generated by pub yadda yadda\n";
+ var result =
+ packages.parse(utf8.encode(packagesFile), Uri.file("/tmp/file.dart"));
+ expect(result.version, 1);
+ expect({for (var p in result.packages) p.name}, <String>{});
+ });
+
+ group("invalid", () {
+ var baseFile = Uri.file("/tmp/file.dart");
+ testThrows(String name, String content) {
+ test(name, () {
+ expect(() => packages.parse(utf8.encode(content), baseFile),
+ throwsA(TypeMatcher<FormatException>()));
+ });
}
- fail("Resolved to $result");
+
+ testThrows("repeated package name", "foo:lib/\nfoo:lib\n");
+ testThrows("no colon", "foo\n");
+ testThrows("empty package name", ":lib/\n");
+ testThrows("dot only package name", ".:lib/\n");
+ testThrows("dot only package name", "..:lib/\n");
+ testThrows("invalid package name character", "f\\o:lib/\n");
+ testThrows("package URI", "foo:package:bar/lib/");
+ testThrows("location with query", "f\\o:lib/?\n");
+ testThrows("location with fragment", "f\\o:lib/#\n");
});
- }
+ });
+
+ group("package_config.json", () {
+ test("valid", () {
+ var packageConfigFile = """
+ {
+ "configVersion": 2,
+ "packages": [
+ {
+ "name": "foo",
+ "rootUri": "file:///foo/",
+ "packageUri": "lib/",
+ "languageVersion": "2.5",
+ "nonstandard": true
+ },
+ {
+ "name": "bar",
+ "rootUri": "/bar/",
+ "packageUri": "lib/",
+ "languageVersion": "100.100"
+ },
+ {
+ "name": "baz",
+ "rootUri": "../",
+ "packageUri": "lib/"
+ }
+ ],
+ "generator": "pub",
+ "other": [42]
+ }
+ """;
+ var config = parsePackageConfigBytes(utf8.encode(packageConfigFile),
+ Uri.parse("file:///tmp/.dart_tool/file.dart"));
+ expect(config.version, 2);
+ expect({for (var p in config.packages) p.name}, {"foo", "bar", "baz"});
+
+ expect(config.resolve(pkg("foo", "foo.dart")),
+ Uri.parse("file:///foo/lib/foo.dart"));
+ expect(config.resolve(pkg("bar", "bar.dart")),
+ Uri.parse("file:///bar/lib/bar.dart"));
+ expect(config.resolve(pkg("baz", "baz.dart")),
+ Uri.parse("file:///tmp/lib/baz.dart"));
+
+ var foo = config["foo"];
+ expect(foo, isNotNull);
+ expect(foo.root, Uri.parse("file:///foo/"));
+ expect(foo.packageUriRoot, Uri.parse("file:///foo/lib/"));
+ expect(foo.languageVersion, "2.5");
+ expect(foo.extraData, {"nonstandard": true});
+
+ var bar = config["bar"];
+ expect(bar, isNotNull);
+ expect(bar.root, Uri.parse("file:///bar/"));
+ expect(bar.packageUriRoot, Uri.parse("file:///bar/lib/"));
+ expect(bar.languageVersion, "100.100");
+ expect(bar.extraData, null);
+
+ var baz = config["baz"];
+ expect(baz, isNotNull);
+ expect(baz.root, Uri.parse("file:///tmp/"));
+ expect(baz.packageUriRoot, Uri.parse("file:///tmp/lib/"));
+ expect(baz.languageVersion, null);
+
+ expect(config.extraData, {
+ "generator": "pub",
+ "other": [42]
+ });
+ });
+
+ test("valid other order", () {
+ // The ordering in the file is not important.
+ var packageConfigFile = """
+ {
+ "generator": "pub",
+ "other": [42],
+ "packages": [
+ {
+ "languageVersion": "2.5",
+ "packageUri": "lib/",
+ "rootUri": "file:///foo/",
+ "name": "foo"
+ },
+ {
+ "packageUri": "lib/",
+ "languageVersion": "100.100",
+ "rootUri": "/bar/",
+ "name": "bar"
+ },
+ {
+ "packageUri": "lib/",
+ "name": "baz",
+ "rootUri": "../"
+ }
+ ],
+ "configVersion": 2
+ }
+ """;
+ var config = parsePackageConfigBytes(utf8.encode(packageConfigFile),
+ Uri.parse("file:///tmp/.dart_tool/file.dart"));
+ expect(config.version, 2);
+ expect({for (var p in config.packages) p.name}, {"foo", "bar", "baz"});
+
+ expect(config.resolve(pkg("foo", "foo.dart")),
+ Uri.parse("file:///foo/lib/foo.dart"));
+ expect(config.resolve(pkg("bar", "bar.dart")),
+ Uri.parse("file:///bar/lib/bar.dart"));
+ expect(config.resolve(pkg("baz", "baz.dart")),
+ Uri.parse("file:///tmp/lib/baz.dart"));
+ expect(config.extraData, {
+ "generator": "pub",
+ "other": [42]
+ });
+ });
+
+ // Check that a few minimal configurations are valid.
+ // These form the basis of invalid tests below.
+ var cfg = '"configVersion":2';
+ var pkgs = '"packages":[]';
+ var name = '"name":"foo"';
+ var root = '"rootUri":"/foo/"';
+ test("minimal", () {
+ var config = parsePackageConfigBytes(utf8.encode("{$cfg,$pkgs}"),
+ Uri.parse("file:///tmp/.dart_tool/file.dart"));
+ expect(config.version, 2);
+ expect(config.packages, isEmpty);
+ });
+ test("minimal package", () {
+ // A package must have a name and a rootUri, the remaining properties
+ // are optional.
+ var config = parsePackageConfigBytes(
+ utf8.encode('{$cfg,"packages":[{$name,$root}]}'),
+ Uri.parse("file:///tmp/.dart_tool/file.dart"));
+ expect(config.version, 2);
+ expect(config.packages.first.name, "foo");
+ });
+
+ group("invalid", () {
+ testThrows(String name, String source) {
+ test(name, () {
+ expect(
+ () => parsePackageConfigBytes(utf8.encode(source),
+ Uri.parse("file:///tmp/.dart_tool/file.dart")),
+ throwsA(TypeMatcher<FormatException>()));
+ });
+ }
+
+ testThrows("comment", '# comment\n {$cfg,$pkgs}');
+ testThrows(".packages file", 'foo:/foo\n');
+ testThrows("no configVersion", '{$pkgs}');
+ testThrows("no packages", '{$cfg}');
+ group("config version:", () {
+ testThrows("null", '{"configVersion":null,$pkgs}');
+ testThrows("string", '{"configVersion":"2",$pkgs}');
+ testThrows("array", '{"configVersion":[2],$pkgs}');
+ });
+ group("packages:", () {
+ testThrows("null", '{$cfg,"packages":null}');
+ testThrows("string", '{$cfg,"packages":"foo"}');
+ testThrows("object", '{$cfg,"packages":{}}');
+ });
+ group("packages entry:", () {
+ testThrows("null", '{$cfg,"packages":[null]}');
+ testThrows("string", '{$cfg,"packages":["foo"]}');
+ testThrows("array", '{$cfg,"packages":[[]]}');
+ });
+ group("package", () {
+ testThrows("no name", '{$cfg,"packages":[{$root}]}');
+ group("name:", () {
+ testThrows("null", '{$cfg,"packages":[{"name":null,$root}]}');
+ testThrows("num", '{$cfg,"packages":[{"name":1,$root}]}');
+ testThrows("object", '{$cfg,"packages":[{"name":{},$root}]}');
+ testThrows("empty", '{$cfg,"packages":[{"name":"",$root}]}');
+ testThrows("one-dot", '{$cfg,"packages":[{"name":".",$root}]}');
+ testThrows("two-dot", '{$cfg,"packages":[{"name":"..",$root}]}');
+ testThrows(
+ "invalid char '\\'", '{$cfg,"packages":[{"name":"\\",$root}]}');
+ testThrows(
+ "invalid char ':'", '{$cfg,"packages":[{"name":":",$root}]}');
+ testThrows(
+ "invalid char ' '", '{$cfg,"packages":[{"name":" ",$root}]}');
+ });
+
+ testThrows("no root", '{$cfg,"packages":[{$name}]}');
+ group("root:", () {
+ testThrows("null", '{$cfg,"packages":[{$name,"rootUri":null}]}');
+ testThrows("num", '{$cfg,"packages":[{$name,"rootUri":1}]}');
+ testThrows("object", '{$cfg,"packages":[{$name,"rootUri":{}}]}');
+ testThrows("fragment", '{$cfg,"packages":[{$name,"rootUri":"x/#"}]}');
+ testThrows("query", '{$cfg,"packages":[{$name,"rootUri":"x/?"}]}');
+ testThrows("package-URI",
+ '{$cfg,"packages":[{$name,"rootUri":"package:x/x/"}]}');
+ });
+ group("package-URI root:", () {
+ testThrows(
+ "null", '{$cfg,"packages":[{$name,$root,"packageUri":null}]}');
+ testThrows("num", '{$cfg,"packages":[{$name,$root,"packageUri":1}]}');
+ testThrows(
+ "object", '{$cfg,"packages":[{$name,$root,"packageUri":{}}]}');
+ testThrows("fragment",
+ '{$cfg,"packages":[{$name,$root,"packageUri":"x/#"}]}');
+ testThrows(
+ "query", '{$cfg,"packages":[{$name,$root,"packageUri":"x/?"}]}');
+ testThrows("package: URI",
+ '{$cfg,"packages":[{$name,$root,"packageUri":"package:x/x/"}]}');
+ testThrows("not inside root",
+ '{$cfg,"packages":[{$name,$root,"packageUri":"../other/"}]}');
+ });
+ group("language version", () {
+ testThrows("null",
+ '{$cfg,"packages":[{$name,$root,"languageVersion":null}]}');
+ testThrows(
+ "num", '{$cfg,"packages":[{$name,$root,"languageVersion":1}]}');
+ testThrows("object",
+ '{$cfg,"packages":[{$name,$root,"languageVersion":{}}]}');
+ testThrows("empty",
+ '{$cfg,"packages":[{$name,$root,"languageVersion":""}]}');
+ testThrows("non number.number",
+ '{$cfg,"packages":[{$name,$root,"languageVersion":"x.1"}]}');
+ testThrows("number.non number",
+ '{$cfg,"packages":[{$name,$root,"languageVersion":"1.x"}]}');
+ testThrows("non number",
+ '{$cfg,"packages":[{$name,$root,"languageVersion":"x"}]}');
+ testThrows("one number",
+ '{$cfg,"packages":[{$name,$root,"languageVersion":"1"}]}');
+ testThrows("three numbers",
+ '{$cfg,"packages":[{$name,$root,"languageVersion":"1.2.3"}]}');
+ testThrows("leading zero first",
+ '{$cfg,"packages":[{$name,$root,"languageVersion":"01.1"}]}');
+ testThrows("leading zero second",
+ '{$cfg,"packages":[{$name,$root,"languageVersion":"1.01"}]}');
+ testThrows("trailing-",
+ '{$cfg,"packages":[{$name,$root,"languageVersion":"1.1-1"}]}');
+ testThrows("trailing+",
+ '{$cfg,"packages":[{$name,$root,"languageVersion":"1.1+1"}]}');
+ });
+ });
+ testThrows("duplicate package name",
+ '{$cfg,"packages":[{$name,$root},{$name,"rootUri":"/other/"}]}');
+ testThrows("same roots",
+ '{$cfg,"packages":[{$name,$root},{"name":"bar",$root}]}');
+ testThrows(
+ "overlapping roots",
+ '{$cfg,"packages":[{$name,$root},'
+ '{"name":"bar","rootUri":"/foo/sub/"}]}');
+ });
+ });
}
-
-Packages doParse(String sample, Uri baseUri,
- {bool allowDefaultPackage = false}) {
- Map<String, Uri> map = parse(sample.codeUnits, baseUri,
- allowDefaultPackage: allowDefaultPackage);
- return new MapPackages(map);
-}
-
-// Valid samples.
-var emptySample = "";
-var commentOnlySample = "# comment only\n";
-var emptyLinesSample = "\n\n\r\n";
-var singleRelativeSample = "foo:../test/\n";
-var singleRelativeSampleNoSlash = "foo:../test\n";
-var singleRelativeSampleNoNewline = "foo:../test/";
-var singleAbsoluteSample = "foo:http://example.com/some/where/\n";
-var singleEmptyPathSample = "foo:\n";
-var singleAbsolutePathSample = "foo:/test/\n";
-var multiRelativeSample = "foo:../test/\nbar:../test2/\n";
-// All valid path segment characters in an URI.
-var allValidChars = r"!$&'()*+,-.0123456789;="
- r"@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~";
-
-var allValidCharsSample = "${allValidChars}:../test/\n";
-
-// Invalid samples.
-var invalid = [
- ":baz.dart", // empty.
- "foobar=baz.dart", // no colon (but an equals, which is not the same)
- ".:../test/", // dot segment
- "..:../test/", // dot-dot segment
- "...:../test/", // dot-dot-dot segment
- "foo/bar:../test/", // slash in name
- "/foo:../test/", // slash at start of name
- "?:../test/", // invalid characters.
- "[:../test/", // invalid characters.
- "x#:../test/", // invalid characters.
-];
diff --git a/test/parse_write_test.dart b/test/parse_write_test.dart
deleted file mode 100644
index 415b479..0000000
--- a/test/parse_write_test.dart
+++ /dev/null
@@ -1,132 +0,0 @@
-// Copyright (c) 2015, 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.
-
-library package_config.parse_write_test;
-
-import "dart:convert" show utf8;
-import "package:package_config/packages_file.dart";
-import "package:test/test.dart";
-
-main() {
- testBase(baseDirString) {
- var baseDir = Uri.parse(baseDirString);
- group("${baseDir.scheme} base", () {
- Uri packagesFile = baseDir.resolve(".packages");
-
- roundTripTest(String name, Map<String, Uri> map) {
- group(name, () {
- test("write with no baseUri", () {
- var content = writeToString(map).codeUnits;
- var resultMap = parse(content, packagesFile);
- expect(resultMap, map);
- });
-
- test("write with base directory", () {
- var content = writeToString(map, baseUri: baseDir).codeUnits;
- var resultMap = parse(content, packagesFile);
- expect(resultMap, map);
- });
-
- test("write with base .packages file", () {
- var content = writeToString(map, baseUri: packagesFile).codeUnits;
- var resultMap = parse(content, packagesFile);
- expect(resultMap, map);
- });
-
- test("write with defaultPackageName", () {
- var content = writeToString(
- {'': Uri.parse('my_pkg')}..addAll(map),
- allowDefaultPackage: true,
- ).codeUnits;
- var resultMap = parse(
- content,
- packagesFile,
- allowDefaultPackage: true,
- );
- expect(resultMap[''].toString(), 'my_pkg');
- expect(
- resultMap,
- {'': Uri.parse('my_pkg')}..addAll(map),
- );
- });
-
- test("write with defaultPackageName (utf8)", () {
- var content = utf8.encode(writeToString(
- {'': Uri.parse('my_pkg')}..addAll(map),
- allowDefaultPackage: true,
- ));
- var resultMap = parse(
- content,
- packagesFile,
- allowDefaultPackage: true,
- );
- expect(resultMap[''].toString(), 'my_pkg');
- expect(
- resultMap,
- {'': Uri.parse('my_pkg')}..addAll(map),
- );
- });
- });
- }
-
- var lowerDir = baseDir.resolve("path3/path4/");
- var higherDir = baseDir.resolve("../");
- var parallelDir = baseDir.resolve("../path3/");
- var rootDir = baseDir.resolve("/");
- var fileDir = Uri.parse("file:///path1/part2/");
- var httpDir = Uri.parse("http://example.com/path1/path2/");
- var otherDir = Uri.parse("other:/path1/path2/");
-
- roundTripTest("empty", {});
- roundTripTest("lower directory", {"foo": lowerDir});
- roundTripTest("higher directory", {"foo": higherDir});
- roundTripTest("parallel directory", {"foo": parallelDir});
- roundTripTest("same directory", {"foo": baseDir});
- roundTripTest("root directory", {"foo": rootDir});
- roundTripTest("file directory", {"foo": fileDir});
- roundTripTest("http directory", {"foo": httpDir});
- roundTripTest("other scheme directory", {"foo": otherDir});
- roundTripTest("multiple same-type directories",
- {"foo": lowerDir, "bar": higherDir, "baz": parallelDir});
- roundTripTest("multiple scheme directories",
- {"foo": fileDir, "bar": httpDir, "baz": otherDir});
- roundTripTest("multiple scheme directories and mutliple same type", {
- "foo": fileDir,
- "bar": httpDir,
- "baz": otherDir,
- "qux": lowerDir,
- "hip": higherDir,
- "dep": parallelDir
- });
- });
- }
-
- testBase("file:///base1/base2/");
- testBase("http://example.com/base1/base2/");
- testBase("other:/base1/base2/");
-
- // Check that writing adds the comment.
- test("write preserves comment", () {
- var comment = "comment line 1\ncomment line 2\ncomment line 3";
- var result = writeToString({}, comment: comment);
- // Comment with "# " before each line and "\n" after last.
- var expectedComment =
- "# comment line 1\n# comment line 2\n# comment line 3\n";
- expect(result, startsWith(expectedComment));
- });
-}
-
-String writeToString(
- Map<String, Uri> map, {
- Uri baseUri,
- String comment,
- bool allowDefaultPackage = false,
-}) {
- var buffer = new StringBuffer();
- write(buffer, map,
- baseUri: baseUri,
- comment: comment,
- allowDefaultPackage: allowDefaultPackage);
- return buffer.toString();
-}
diff --git a/test/src/util.dart b/test/src/util.dart
new file mode 100644
index 0000000..ec4e5e8
--- /dev/null
+++ b/test/src/util.dart
@@ -0,0 +1,109 @@
+// 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 "dart:io";
+import 'dart:typed_data';
+
+import "package:path/path.dart" as path;
+import "package:test/test.dart";
+
+/// Creates a directory structure from [description] and runs [fileTest].
+///
+/// Description is a map, each key is a file entry. If the value is a map,
+/// it's a subdirectory, otherwise it's a file and the value is the content
+/// as a string.
+/// Introduces a group to hold the [setUp]/[tearDown] logic.
+void fileTest(String name, Map<String, Object> description,
+ void fileTest(Directory directory)) {
+ group("file-test", () {
+ Directory tempDir = Directory.systemTemp.createTempSync("pkgcfgtest");
+ setUp(() {
+ _createFiles(tempDir, description);
+ });
+ tearDown(() {
+ tempDir.deleteSync(recursive: true);
+ });
+ test(name, () => fileTest(tempDir));
+ });
+}
+
+/// Creates a set of files under a new temporary directory.
+/// Returns the temporary directory.
+///
+/// The [description] is a map from file names to content.
+/// If the content is again a map, it represents a subdirectory
+/// with the content as description.
+/// Otherwise the content should be a string,
+/// which is written to the file as UTF-8.
+Directory createTestFiles(Map<String, Object> description) {
+ var target = Directory.systemTemp.createTempSync("pkgcfgtest");
+ _createFiles(target, description);
+ return target;
+}
+
+// Creates temporary files in the target directory.
+void _createFiles(Directory target, Map<Object, Object> description) {
+ description.forEach((name, content) {
+ var entryName = path.join(target.path, "$name");
+ if (content is Map<Object, Object>) {
+ _createFiles(Directory(entryName)..createSync(), content);
+ } else {
+ File(entryName).writeAsStringSync(content, flush: true);
+ }
+ });
+}
+
+/// Creates a [Directory] for a subdirectory of [parent].
+Directory subdir(Directory parent, String dirName) =>
+ Directory(path.joinAll([parent.path, ...dirName.split("/")]));
+
+/// Creates a [File] for an entry in the [directory] directory.
+File dirFile(Directory directory, String fileName) =>
+ File(path.join(directory.path, fileName));
+
+/// Creates a package: URI.
+Uri pkg(String packageName, String packagePath) {
+ var path =
+ "$packageName${packagePath.startsWith('/') ? "" : "/"}$packagePath";
+ return new Uri(scheme: "package", path: path);
+}
+
+// Remove if not used.
+String configFromPackages(List<List<String>> packages) => """
+{
+ "configVersion": 2,
+ "packages": [
+${packages.map((nu) => """
+ {
+ "name": "${nu[0]}",
+ "rootUri": "${nu[1]}"
+ }""").join(",\n")}
+ ]
+}
+""";
+
+/// Mimics a directory structure of [description] and runs [fileTest].
+///
+/// Description is a map, each key is a file entry. If the value is a map,
+/// it's a subdirectory, otherwise it's a file and the value is the content
+/// as a string.
+void loaderTest(String name, Map<String, Object> description,
+ void loaderTest(Uri root, Future<Uint8List> loader(Uri uri))) {
+ Uri root = Uri(scheme: "test", path: "/");
+ Future<Uint8List> loader(Uri uri) async {
+ var path = uri.path;
+ if (!uri.isScheme("test") || !path.startsWith("/")) return null;
+ var parts = path.split("/");
+ dynamic value = description;
+ for (int i = 1; i < parts.length; i++) {
+ if (value is! Map<String, dynamic>) return null;
+ value = value[parts[i]];
+ }
+ if (value is String) return utf8.encode(value);
+ return null;
+ }
+
+ test(name, () => loaderTest(root, loader));
+}