blob: 662435ac30b7047230d4cca4374d8660a55aa5a5 [file]
// Copyright (c) 2023, 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.
/// Find extensions with [findExtensions].
library;
import 'dart:collection' show UnmodifiableListView;
import 'dart:io' show File, IOException;
import 'dart:isolate' show Isolate;
import 'src/io.dart';
import 'src/package_config.dart';
import 'src/registry.dart';
import 'src/yaml_config_format.dart';
export 'src/package_config.dart' show PackageConfigException, findPackageConfig;
/// Information about an extension for target package.
final class Extension {
/// Name of the package providing an extension.
final String package;
/// Absolute path to the package root.
///
/// This folder usually contains a `lib/` folder and a `pubspec.yaml`
/// (assuming dependencies are fetched using the pub package manager).
///
/// **Examples:** If `foo` is installed in pub-cache this would be:
/// * `/home/my_user/.pub-cache/hosted/pub.dev/foo-1.0.0/`
///
/// See `rootUri` in the [specification for `package_config.json`][1],
/// for details.
///
/// [1]: https://github.com/dart-lang/language/blob/main/accepted/2.8/language-versioning/package-config-file-v2.md
final Uri rootUri;
/// Path to the library import path relative to [rootUri].
///
/// In Dart code the `package:<package>/<path>` will be resolved as
/// `<rootUri>/<packageUri>/<path>`.
///
/// If dependencies are installed using `dart pub`, then this is
/// **always** `lib/`.
///
/// See `packageUri` in the [specification for `package_config.json`][1],
/// for details.
///
/// [1]: https://github.com/dart-lang/language/blob/main/accepted/2.8/language-versioning/package-config-file-v2.md
final Uri packageUri;
/// Contents of `extension/<targetPackage>/config.yaml` parsed as YAML and
/// converted to JSON compatible types.
///
/// If parsing YAML from this file failed, then no [Extension] entry
/// will exist.
///
/// This field is always a structure consisting of the following types:
/// * `null`,
/// * [bool] (`true` or `false`),
/// * [String],
/// * [num] ([int] or [double]),
/// * `List<Object?>`, and,
/// * `Map<String, Object?>`.
final Map<String, Object?> config;
Extension._({
required this.package,
required this.rootUri,
required this.packageUri,
required this.config,
});
}
/// Find extensions for [targetPackage] provided by packages in
/// `.dart_tool/package_config.json`.
///
/// ## Locating `.dart_tool/package_config.json`
///
/// This method requires the location of the [packageConfig], unless the current
/// isolate has been setup for package resolution.
/// Notably, Dart programs compiled for AOT cannot find their own
/// `package_config.json`.
///
/// If operating on a project that isn't the current project, for example, if
/// you are developing a tool that users are globally activating and then
/// running against their own projects, and you wish to detect extensions within
/// their projects, then you must specify the path the
/// `.dart_tool/package_config.json` for the users project as [packageConfig].
///
/// The [packageConfig] parameter must reference a file, absolute or
/// relative-path, may use the `file://` scheme. This method throws, if
/// [packageConfig] is not a valid [Uri] for a file-path.
///
/// ## Detection of extensions
///
/// An extension for [targetPackage] is detected in `package:foo` if `foo`
/// contains `extension/<targetPackage>/config.yaml`, and the contents of this
/// file is valid YAML, that can be represented as JSON.
///
/// ### Caching results
///
/// When [useCache] is `true` then the detected extensions will be cached
/// in `.dart_tool/extension_discovery/<targetPackage>.json`.
/// This function will compare modification timestamps of
/// `.dart_tool/package_config.json` with the cache file, before reusing cached
/// results.
/// This function will also treat relative path-dependencies as mutable
/// packages, and check such packages for extensions every time [findExtensions]
/// is called. Notably, it'll compare the modification time of the
/// `extension/<targetPackage>/config.yaml` file, to ensure that it's older than
/// the extension cache file. Otherwise, it'll reload the extension
/// configuration.
///
/// ## Exceptions
///
/// This method will throw [PackageConfigException], if the
/// `.dart_tool/package_config.json` file specified in [packageConfig] could not
/// be loaded or is invalid. This usually happens if dependencies are not
/// resolved, and users can probably address it by running `dart pub get`.
///
/// But, **do consider catch** [PackageConfigException] and handling the failure
/// to load extensions appropriately.
///
/// This method will throw an [Error] if [packageConfig] is not specified, and
/// the current isolate isn't configured for package resolution.
Future<List<Extension>> findExtensions(
String targetPackage, {
bool useCache = true,
Uri? packageConfig,
}) async {
packageConfig ??= await Isolate.packageConfig;
if (packageConfig == null) {
throw UnsupportedError(
'packageConfigUri must be provided, if not running in JIT mode',
);
}
if ((packageConfig.hasScheme && !packageConfig.isScheme('file')) ||
packageConfig.hasEmptyPath ||
packageConfig.hasFragment ||
packageConfig.hasPort ||
packageConfig.hasQuery) {
throw ArgumentError.value(
packageConfig,
'packageConfig',
'must be a file:// URI',
);
}
// Always normalize to an absolute URI
final packageConfigUri = File.fromUri(packageConfig).absolute.uri;
return await _findExtensions(
targetPackage: targetPackage,
useCache: useCache,
packageConfigUri: packageConfigUri,
);
}
/// Find extensions with normalized arguments.
Future<List<Extension>> _findExtensions({
required String targetPackage,
required bool useCache,
required Uri packageConfigUri,
}) async {
final packageConfigFile = File.fromUri(packageConfigUri);
final registryFile = File.fromUri(packageConfigFile.parent.uri.resolve(
'extension_discovery/$targetPackage.json',
));
Registry? registry;
final registryStat = registryFile.statSync();
if (registryStat.isFileOrLink && useCache) {
final packageConfigStat = packageConfigFile.statSync();
if (!packageConfigStat.isFileOrLink) {
throw packageConfigNotFound(packageConfigUri);
}
if (packageConfigStat.isPossiblyModifiedAfter(registryStat.modified)) {
await registryFile.tryDelete();
} else {
registry = await loadRegistry(registryFile);
}
}
final configFileName = 'extension/$targetPackage/config.yaml';
var registryUpdated = false;
if (registry != null) {
// Update mutable entries in registry
for (var i = 0; i < registry.length; i++) {
final p = registry[i];
if (p.rootUri.hasAbsolutePath) continue;
final rootUri = packageConfigUri.resolveUri(p.rootUri);
final configFile = File.fromUri(rootUri.resolve(configFileName));
final configStat = configFile.statSync();
if (configStat.isFileOrLink) {
if (configStat.isPossiblyModifiedAfter(registryStat.modified)) {
try {
registryUpdated = true;
registry[i] = (
package: p.package,
rootUri: p.rootUri,
packageUri: p.packageUri,
config: parseYamlFromConfigFile(await configFile.readAsString()),
);
continue;
} on FormatException {
// pass
} on IOException {
// pass
}
registryUpdated = true;
registry[i] = (
package: p.package,
rootUri: p.rootUri,
packageUri: p.packageUri,
config: null,
);
}
} else {
// If there is no file present, but registry says there is then we need
// to update the registry.
if (p.config != null) {
registryUpdated = true;
registry[i] = (
package: p.package,
rootUri: p.rootUri,
packageUri: p.packageUri,
config: null,
);
}
}
}
} else {
// Load packages from package_config.json
final packages = await loadPackageConfig(packageConfigFile);
registryUpdated = true;
registry = (await Future.wait(packages.map((p) async {
try {
final rootUri = packageConfigUri.resolveUri(p.rootUri);
final configFile = File.fromUri(rootUri.resolve(configFileName));
final configStat = configFile.statSync();
if (configStat.isFileOrLink) {
return (
package: p.name,
rootUri: p.rootUri,
packageUri: p.packageUri,
config: parseYamlFromConfigFile(await configFile.readAsString()),
);
}
} on FormatException {
// pass
} on IOException {
// pass
}
if (!p.rootUri.hasAbsolutePath) {
return (
package: p.name,
rootUri: p.rootUri,
packageUri: p.packageUri,
config: null,
);
}
return null;
})))
.whereType<RegistryEntry>()
.toList(growable: false);
}
// Save registry
if (registryUpdated && useCache) {
await saveRegistry(registryFile, registry);
}
return UnmodifiableListView(
registry
.where((e) => e.config != null)
.map((e) => Extension._(
package: e.package,
rootUri: packageConfigUri.resolveUri(e.rootUri),
packageUri: e.packageUri,
config: e.config!,
))
.toList(growable: false),
);
}