| // 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), |
| ); |
| } |