// Copyright (c) 2014, 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 'package:package_config/package_config.dart'; import 'package:path/path.dart' as p; /// [Resolver] resolves imports with respect to a given environment. class Resolver { @Deprecated('Use Resolver.create') Resolver({this.packagesPath, this.sdkRoot}) : _packages = packagesPath != null ? _parsePackages(packagesPath) : null, packagePath = null; Resolver._( {this.packagesPath, this.packagePath, this.sdkRoot, Map? packages}) : _packages = packages; static Future create({ String? packagesPath, String? packagePath, String? sdkRoot, }) async { return Resolver._( packagesPath: packagesPath, packagePath: packagePath, sdkRoot: sdkRoot, packages: packagesPath != null ? _parsePackages(packagesPath) : (packagePath != null ? await _parsePackage(packagePath) : null), ); } final String? packagesPath; final String? packagePath; final String? sdkRoot; final List failed = []; final Map? _packages; /// Returns the absolute path wrt. to the given environment or null, if the /// import could not be resolved. String? resolve(String scriptUri) { final uri = Uri.parse(scriptUri); if (uri.scheme == 'dart') { final sdkRoot = this.sdkRoot; if (sdkRoot == null) { // No sdk-root given, do not resolve dart: URIs. return null; } String filePath; if (uri.pathSegments.length > 1) { var path = uri.pathSegments[0]; // Drop patch files, since we don't have their source in the compiled // SDK. if (path.endsWith('-patch')) { failed.add('$uri'); return null; } // Canonicalize path. For instance: _collection-dev => _collection_dev. path = path.replaceAll('-', '_'); final pathSegments = [ sdkRoot, path, ...uri.pathSegments.sublist(1), ]; filePath = p.joinAll(pathSegments); } else { // Resolve 'dart:something' to be something/something.dart in the SDK. final lib = uri.path; filePath = p.join(sdkRoot, lib, '$lib.dart'); } return resolveSymbolicLinks(filePath); } if (uri.scheme == 'package') { final packages = _packages; if (packages == null) { return null; } final packageName = uri.pathSegments[0]; final packageUri = packages[packageName]; if (packageUri == null) { failed.add('$uri'); return null; } final packagePath = p.fromUri(packageUri); final pathInPackage = p.joinAll(uri.pathSegments.sublist(1)); return resolveSymbolicLinks(p.join(packagePath, pathInPackage)); } if (uri.scheme == 'file') { return resolveSymbolicLinks(p.fromUri(uri)); } // We cannot deal with anything else. failed.add('$uri'); return null; } /// Returns a canonicalized path, or `null` if the path cannot be resolved. String? resolveSymbolicLinks(String path) { final normalizedPath = p.normalize(path); final type = FileSystemEntity.typeSync(normalizedPath, followLinks: true); if (type == FileSystemEntityType.notFound) return null; return File(normalizedPath).resolveSymbolicLinksSync(); } static Map _parsePackages(String packagesPath) { final content = File(packagesPath).readAsStringSync(); final packagesUri = p.toUri(packagesPath); final parsed = PackageConfig.parseString(content, Uri.base.resolveUri(packagesUri)); return { for (var package in parsed.packages) package.name: package.packageUriRoot }; } static Future?> _parsePackage(String packagePath) async { final parsed = await findPackageConfig(Directory(packagePath)); if (parsed == null) return null; return { for (var package in parsed.packages) package.name: package.packageUriRoot }; } } /// Bazel URI resolver. class BazelResolver extends Resolver { /// Creates a Bazel resolver with the specified workspace path, if any. BazelResolver({this.workspacePath = ''}); final String workspacePath; /// Returns the absolute path wrt. to the given environment or null, if the /// import could not be resolved. @override String? resolve(String scriptUri) { final uri = Uri.parse(scriptUri); if (uri.scheme == 'dart') { // Ignore the SDK return null; } if (uri.scheme == 'package') { // TODO(cbracken) belongs in a Bazel package return _resolveBazelPackage(uri.pathSegments); } if (uri.scheme == 'file') { final runfilesPathSegment = '.runfiles/$workspacePath'.replaceAll(RegExp(r'/*$'), '/'); final runfilesPos = uri.path.indexOf(runfilesPathSegment); if (runfilesPos >= 0) { final pathStart = runfilesPos + runfilesPathSegment.length; return uri.path.substring(pathStart); } return null; } if (uri.scheme == 'https' || uri.scheme == 'http') { return _extractHttpPath(uri); } // We cannot deal with anything else. failed.add('$uri'); return null; } String _extractHttpPath(Uri uri) { final packagesPos = uri.pathSegments.indexOf('packages'); if (packagesPos >= 0) { final workspacePath = uri.pathSegments.sublist(packagesPos + 1); return _resolveBazelPackage(workspacePath); } return uri.pathSegments.join('/'); } String _resolveBazelPackage(List pathSegments) { // TODO(cbracken) belongs in a Bazel package final packageName = pathSegments[0]; final pathInPackage = pathSegments.sublist(1).join('/'); final packagePath = packageName.contains('.') ? packageName.replaceAll('.', '/') : 'third_party/dart/$packageName'; return '$packagePath/lib/$pathInPackage'; } } /// Loads the lines of imported resources. class Loader { final List failed = []; /// Loads an imported resource and returns a [Future] with a [List] of lines. /// Returns `null` if the resource could not be loaded. Future?> load(String path) async { try { // Ensure `readAsLines` runs within the try block so errors are caught. return await File(path).readAsLines(); } catch (_) { failed.add(path); return null; } } /// Loads an imported resource and returns a [List] of lines. /// Returns `null` if the resource could not be loaded. List? loadSync(String path) { try { // Ensure `readAsLinesSync` runs within the try block so errors are // caught. return File(path).readAsLinesSync(); } catch (_) { failed.add(path); return null; } } }