blob: 88e81dc6750120da04de425a888d0f0428c0a14c [file] [log] [blame]
// 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:dwds/src/loaders/strategy.dart';
import 'package:logging/logging.dart';
import 'package:package_config/package_config.dart';
import 'package:path/path.dart' as p;
/// The URI for a particular Dart file, able to canonicalize from various
/// different representations.
class DartUri {
DartUri._(this.serverPath);
/// Accepts various forms of URI and can convert between forms.
///
/// The accepted forms are:
///
/// - package:packageName/pathUnderLib/file.dart
/// - org-dartlang-app:///prefix/path/file.dart, where prefix is ignored.
/// e.g. org-dartlang-app:example/hello_world/main.dart,
/// - /packages/packageName/foo.dart, the web server form of a package URI,
/// e.g. /packages/path/src/utils.dart
/// - /path/foo.dart or path/foo.dart, e.g. /hello_world/web/main.dart, where
/// path is a web server path and so relative to the directory being
/// served, not to the package.
///
/// The optional [root] is the directory the app is served from.
factory DartUri(String uri, [String? root]) {
// TODO(annagrin): Support creating DartUris from `dart:` uris.
// Issue: https://github.com/dart-lang/webdev/issues/1584
if (uri.startsWith('org-dartlang-app:') || uri.startsWith('google3:')) {
return DartUri._fromDartLangUri(uri);
}
if (uri.startsWith('package:')) {
return DartUri._fromPackageUri(uri, root: root);
}
if (uri.startsWith('file:')) {
return DartUri._fromFileUri(uri, root: root);
}
if (uri.startsWith('packages/') || uri.startsWith('/packages/')) {
return DartUri._fromRelativePath(uri, root: root);
}
if (uri.startsWith('/')) {
return DartUri._fromRelativePath(uri);
}
if (uri.startsWith('http:') || uri.startsWith('https:')) {
return DartUri(Uri.parse(uri).path);
}
throw FormatException('Unsupported URI form: $uri');
}
@override
String toString() => 'DartUri: $serverPath';
/// Construct from a package: URI
factory DartUri._fromDartLangUri(String uri) {
var serverPath = globalLoadStrategy.serverPathForAppUri(uri);
if (serverPath == null) {
_logger.severe('Cannot find server path for $uri');
serverPath = uri;
}
return DartUri._(serverPath);
}
/// Construct from a package: URI
factory DartUri._fromPackageUri(String uri, {String? root}) {
var serverPath = globalLoadStrategy.serverPathForAppUri(uri);
if (serverPath == null) {
_logger.severe('Cannot find server path for $uri');
serverPath = uri;
}
return DartUri._fromRelativePath(serverPath, root: root);
}
/// Construct from a file: URI
factory DartUri._fromFileUri(String uri, {String? root}) {
final libraryName = _resolvedUriToUri[uri];
if (libraryName != null) return DartUri(libraryName, root);
// This is not one of our recorded libraries.
throw ArgumentError.value(uri, 'uri', 'Unknown library');
}
/// Construct from a path, relative to the directory being served.
factory DartUri._fromRelativePath(String uri, {String? root}) {
uri = uri[0] == '.' ? uri.substring(1) : uri;
uri = uri[0] == '/' ? uri.substring(1) : uri;
if (root != null) {
return DartUri._fromRelativePath(p.url.join(root, uri));
}
return DartUri._(uri);
}
/// The canonical web server path part of the URI.
///
/// This is a relative path, which can be used to fetch the corresponding file
/// from the server. For example, 'hello_world/main.dart' or
/// 'packages/path/src/utils.dart'.
final String serverPath;
static final _logger = Logger('DartUri');
/// The way we resolve file: URLs into package: URLs
static PackageConfig? _packageConfig;
/// All of the known absolute library paths, indexed by their library URL.
///
/// Examples:
///
/// We are assuming that all library uris are coming from
/// https://github.com/dart-lang/sdk/blob/main/runtime/vm/service/service.md#getscripts)
/// and can be translated to their absolute paths and back.
///
/// dart:html <->
/// org-dartlang-sdk:///sdk/lib/html/html.dart
/// (not supported, issue: https://github.com/dart-lang/webdev/issues/1457)
///
/// org-dartlang-app:///example/hello_world/main.dart <->
/// file:///source/webdev/fixtures/_test/example/hello_world/main.dart,
///
/// org-dartlang-app:///example/hello_world/part.dart <->
/// file:///source/webdev/fixtures/_test/example/hello_world/part.dart,
///
/// package:path/path.dart <->
/// file:///.pub-cache/hosted/pub.dartlang.org/path-1.8.0/lib/path.dart,
///
/// package:path/src/path_set.dart <->
/// file:///.pub-cache/hosted/pub.dartlang.org/path-1.8.0/lib/src/path_set.dart,
static final Map<String, String> _uriToResolvedUri = {};
/// All of the known libraries, indexed by their absolute file URL.
static final Map<String, String> _resolvedUriToUri = {};
/// Returns package, app, or dart uri for a resolved path.
static String? toPackageUri(String uri) => _resolvedUriToUri[uri];
/// Returns resolved path for a package, app, or dart uri.
static String? toResolvedUri(String uri) => _uriToResolvedUri[uri];
/// The directory in which we're running.
///
/// We store this here because for tests we may want to act as if we're
/// running in the directory of a target package, even if the current
/// directory of the tests is actually the main dwds directory.
static String currentDirectory = p.current;
/// The current directory as a file: Uri, saved here to avoid re-computing.
static String currentDirectoryUri = '${p.toUri(currentDirectory)}';
/// Record library and script uris to enable resolving library and script paths.
static Future<void> initialize() async {
final packagesUri =
p.toUri(p.join(currentDirectory, '.dart_tool/package_config.json'));
clear();
await _loadPackageConfig(packagesUri);
}
/// Clear the uri resolution tables.
static void clear() {
_packageConfig = null;
_resolvedUriToUri.clear();
_uriToResolvedUri.clear();
}
/// Record all of the libraries, indexed by their absolute file: URI.
static void recordAbsoluteUris(Iterable<String> libraryUris) {
for (var uri in libraryUris) {
_recordAbsoluteUri(uri);
}
}
/// Load the .dart_tool/package_config.json file associated with the running
/// application so we can resolve file URLs into package: URLs appropriately.
static Future<void> _loadPackageConfig(Uri uri) async {
_packageConfig = await loadPackageConfigUri(uri, onError: (e) {
_logger.warning('Cannot read packages spec: $uri', e);
});
}
/// Record the library represented by package: or org-dartlang-app: uris
/// indexed by absolute file: URI.
static void _recordAbsoluteUri(String libraryUri) {
final uri = Uri.parse(libraryUri);
if (uri.scheme.isEmpty && !uri.path.endsWith('.dart')) {
// ignore non-dart files
return;
}
String? libraryPath;
switch (uri.scheme) {
case 'dart':
// TODO(annagrin): Support resolving `dart:` uris.
// Issue: https://github.com/dart-lang/webdev/issues/1584
return;
case 'org-dartlang-app':
case 'google3':
// Both currentDirectoryUri and the libraryUri path should have '/'
// separators, so we can join them as url paths to get the absolute file
// url.
libraryPath = p.url.join(currentDirectoryUri, uri.path.substring(1));
break;
case 'package':
libraryPath = _packageConfig?.resolve(uri)?.toString();
break;
default:
throw ArgumentError.value(libraryUri, 'URI scheme not allowed');
}
if (libraryPath != null) {
_uriToResolvedUri[libraryUri] = libraryPath;
_resolvedUriToUri[libraryPath] = libraryUri;
} else {
_logger.fine('Unresolved uri: $uri');
}
}
}