blob: 6415e6c13bfdb52a51f40c757c522520cfaf69d2 [file] [log] [blame]
// 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:convert';
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 {
Resolver({String? packagesPath, this.sdkRoot})
: packagesPath = packagesPath,
_packages = packagesPath != null ? _parsePackages(packagesPath) : null;
final String? packagesPath;
final String? sdkRoot;
final List<String> failed = [];
final Map<String, Uri>? _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 = this._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<String, Uri> _parsePackages(String packagesPath) {
final content = File(packagesPath).readAsStringSync();
try {
final parsed =
PackageConfig.parseString(content, Uri.base.resolve(packagesPath));
return {
for (var package in parsed.packages)
package.name: package.packageUriRoot
};
} on FormatException catch (_) {
// It was probably an old style .packages file
final lines = LineSplitter.split(content);
final packageMap = <String, Uri>{};
for (var line in lines) {
if (line.startsWith('#')) continue;
final firstColon = line.indexOf(':');
if (firstColon == -1) {
throw FormatException(
'Unexpected package config format, expected an old style '
'.packages file or new style package_config.json file.',
content);
}
packageMap[line.substring(0, firstColon)] =
Uri.parse(line.substring(firstColon + 1, line.length));
}
return packageMap;
}
}
}
/// 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<String> 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<String> failed = [];
/// Loads an imported resource and returns a [Future] with a [List] of lines.
/// Returns `null` if the resource could not be loaded.
Future<List<String>?> 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;
}
}
}