2018-04-28 01:57:37 +02:00
|
|
|
// Copyright 2018 Google Inc. Use of this source code is governed by an
|
|
|
|
// MIT-style license that can be found in the LICENSE file or at
|
|
|
|
// https://opensource.org/licenses/MIT.
|
|
|
|
|
|
|
|
import 'dart:async';
|
|
|
|
|
2018-10-12 00:06:26 +02:00
|
|
|
import 'package:collection/collection.dart';
|
2018-06-14 01:57:28 +02:00
|
|
|
import 'package:path/path.dart' as p;
|
2018-04-28 01:57:37 +02:00
|
|
|
import 'package:tuple/tuple.dart';
|
|
|
|
|
|
|
|
import 'ast/sass.dart';
|
|
|
|
import 'importer.dart';
|
|
|
|
import 'logger.dart';
|
|
|
|
import 'sync_package_resolver.dart';
|
|
|
|
import 'utils.dart'; // ignore: unused_import
|
|
|
|
|
|
|
|
/// An in-memory cache of parsed stylesheets that have been imported by Sass.
|
|
|
|
class AsyncImportCache {
|
|
|
|
/// A cache that contains no importers.
|
|
|
|
static const none = const AsyncImportCache._none();
|
|
|
|
|
|
|
|
/// The importers to use when loading new Sass files.
|
|
|
|
final List<AsyncImporter> _importers;
|
|
|
|
|
|
|
|
/// The logger to use to emit warnings when parsing stylesheets.
|
|
|
|
final Logger _logger;
|
|
|
|
|
|
|
|
/// The canonicalized URLs for each non-canonical URL.
|
|
|
|
///
|
2018-06-21 02:43:40 +02:00
|
|
|
/// This map's values are the same as the return value of [canonicalize].
|
|
|
|
///
|
2018-04-28 01:57:37 +02:00
|
|
|
/// This cache isn't used for relative imports, because they're
|
|
|
|
/// context-dependent.
|
2018-06-21 02:43:40 +02:00
|
|
|
final Map<Uri, Tuple3<AsyncImporter, Uri, Uri>> _canonicalizeCache;
|
2018-04-28 01:57:37 +02:00
|
|
|
|
|
|
|
/// The parsed stylesheets for each canonicalized import URL.
|
|
|
|
final Map<Uri, Stylesheet> _importCache;
|
|
|
|
|
|
|
|
/// Creates an import cache that resolves imports using [importers].
|
|
|
|
///
|
|
|
|
/// Imports are resolved by trying, in order:
|
|
|
|
///
|
|
|
|
/// * Each importer in [importers].
|
|
|
|
///
|
|
|
|
/// * Each load path in [loadPaths]. Note that this is a shorthand for adding
|
|
|
|
/// [FilesystemImporter]s to [importers].
|
|
|
|
///
|
|
|
|
/// * `package:` resolution using [packageResolver], which is a
|
|
|
|
/// [`SyncPackageResolver`][] from the `package_resolver` package. Note that
|
|
|
|
/// this is a shorthand for adding a [PackageImporter] to [importers].
|
|
|
|
///
|
|
|
|
/// [`SyncPackageResolver`]: https://www.dartdocs.org/documentation/package_resolver/latest/package_resolver/SyncPackageResolver-class.html
|
|
|
|
AsyncImportCache(Iterable<AsyncImporter> importers,
|
|
|
|
{Iterable<String> loadPaths,
|
|
|
|
SyncPackageResolver packageResolver,
|
|
|
|
Logger logger})
|
|
|
|
: _importers = _toImporters(importers, loadPaths, packageResolver),
|
|
|
|
_logger = logger ?? const Logger.stderr(),
|
|
|
|
_canonicalizeCache = {},
|
|
|
|
_importCache = {};
|
|
|
|
|
|
|
|
/// Converts the user's [importers], [loadPaths], and [packageResolver]
|
|
|
|
/// options into a single list of importers.
|
|
|
|
static List<AsyncImporter> _toImporters(Iterable<AsyncImporter> importers,
|
|
|
|
Iterable<String> loadPaths, SyncPackageResolver packageResolver) {
|
|
|
|
var list = importers?.toList() ?? [];
|
|
|
|
if (loadPaths != null) {
|
|
|
|
list.addAll(loadPaths.map((path) => new FilesystemImporter(path)));
|
|
|
|
}
|
|
|
|
if (packageResolver != null) {
|
|
|
|
list.add(new PackageImporter(packageResolver));
|
|
|
|
}
|
|
|
|
return list;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Creates a cache that contains no importers.
|
|
|
|
const AsyncImportCache._none()
|
|
|
|
: _importers = const [],
|
|
|
|
_logger = const Logger.stderr(),
|
|
|
|
_canonicalizeCache = const {},
|
|
|
|
_importCache = const {};
|
|
|
|
|
2018-06-21 02:43:40 +02:00
|
|
|
/// Canonicalizes [url] according to one of this cache's importers.
|
|
|
|
///
|
|
|
|
/// Returns the importer that was used to canonicalize [url], the canonical
|
|
|
|
/// URL, and the URL that was passed to the importer (which may be resolved
|
|
|
|
/// relative to [baseUrl] if it's passed).
|
2018-04-28 01:57:37 +02:00
|
|
|
///
|
|
|
|
/// If [baseImporter] is non-`null`, this first tries to use [baseImporter] to
|
|
|
|
/// canonicalize [url] (resolved relative to [baseUrl] if it's passed).
|
|
|
|
///
|
|
|
|
/// If any importers understand [url], returns that importer as well as the
|
|
|
|
/// canonicalized URL. Otherwise, returns `null`.
|
2018-06-21 02:43:40 +02:00
|
|
|
Future<Tuple3<AsyncImporter, Uri, Uri>> canonicalize(Uri url,
|
2018-04-28 01:57:37 +02:00
|
|
|
[AsyncImporter baseImporter, Uri baseUrl]) async {
|
|
|
|
if (baseImporter != null) {
|
2018-06-21 02:43:40 +02:00
|
|
|
var resolvedUrl = baseUrl != null ? baseUrl.resolveUri(url) : url;
|
2018-10-12 00:06:26 +02:00
|
|
|
var canonicalUrl = await _canonicalize(baseImporter, resolvedUrl);
|
2018-06-21 02:43:40 +02:00
|
|
|
if (canonicalUrl != null) {
|
|
|
|
return new Tuple3(baseImporter, canonicalUrl, resolvedUrl);
|
|
|
|
}
|
2018-04-28 01:57:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return await putIfAbsentAsync(_canonicalizeCache, url, () async {
|
|
|
|
for (var importer in _importers) {
|
2018-10-12 00:06:26 +02:00
|
|
|
var canonicalUrl = await _canonicalize(importer, url);
|
2018-06-21 02:43:40 +02:00
|
|
|
if (canonicalUrl != null) {
|
|
|
|
return new Tuple3(importer, canonicalUrl, url);
|
|
|
|
}
|
2018-04-28 01:57:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-10-12 00:06:26 +02:00
|
|
|
/// Calls [importer.canonicalize] and prints a deprecation warning if it
|
|
|
|
/// returns a relative URL.
|
|
|
|
Future<Uri> _canonicalize(AsyncImporter importer, Uri url) async {
|
|
|
|
var result = await importer.canonicalize(url);
|
|
|
|
if (result?.scheme == '') {
|
|
|
|
_logger.warn("""
|
|
|
|
Importer $importer canonicalized $url to $result.
|
|
|
|
Relative canonical URLs are deprecated and will eventually be disallowed.
|
|
|
|
""", deprecation: true);
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2018-04-28 01:57:37 +02:00
|
|
|
/// Tries to import [url] using one of this cache's importers.
|
|
|
|
///
|
|
|
|
/// If [baseImporter] is non-`null`, this first tries to use [baseImporter] to
|
|
|
|
/// import [url] (resolved relative to [baseUrl] if it's passed).
|
|
|
|
///
|
|
|
|
/// If any importers can import [url], returns that importer as well as the
|
|
|
|
/// parsed stylesheet. Otherwise, returns `null`.
|
|
|
|
///
|
|
|
|
/// Caches the result of the import and uses cached results if possible.
|
|
|
|
Future<Tuple2<AsyncImporter, Stylesheet>> import(Uri url,
|
|
|
|
[AsyncImporter baseImporter, Uri baseUrl]) async {
|
|
|
|
var tuple = await canonicalize(url, baseImporter, baseUrl);
|
|
|
|
if (tuple == null) return null;
|
2018-06-21 02:43:40 +02:00
|
|
|
var stylesheet =
|
|
|
|
await importCanonical(tuple.item1, tuple.item2, tuple.item3);
|
2018-04-28 01:57:37 +02:00
|
|
|
return new Tuple2(tuple.item1, stylesheet);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Tries to load the canonicalized [canonicalUrl] using [importer].
|
|
|
|
///
|
|
|
|
/// If [importer] can import [canonicalUrl], returns the imported [Stylesheet].
|
|
|
|
/// Otherwise returns `null`.
|
|
|
|
///
|
|
|
|
/// If passed, the [originalUrl] represents the URL that was canonicalized
|
|
|
|
/// into [canonicalUrl]. It's used as the URL for the parsed stylesheet, which
|
|
|
|
/// is in turn used in error reporting.
|
|
|
|
///
|
|
|
|
/// Caches the result of the import and uses cached results if possible.
|
|
|
|
Future<Stylesheet> importCanonical(AsyncImporter importer, Uri canonicalUrl,
|
|
|
|
[Uri originalUrl]) async {
|
|
|
|
return await putIfAbsentAsync(_importCache, canonicalUrl, () async {
|
|
|
|
var result = await importer.load(canonicalUrl);
|
|
|
|
if (result == null) return null;
|
2018-08-11 00:58:15 +02:00
|
|
|
return new Stylesheet.parse(result.contents, result.syntax,
|
2018-10-12 00:06:26 +02:00
|
|
|
// For backwards-compatibility, relative canonical URLs are resolved
|
|
|
|
// relative to [originalUrl].
|
|
|
|
url: originalUrl == null
|
|
|
|
? canonicalUrl
|
|
|
|
: originalUrl.resolveUri(canonicalUrl),
|
|
|
|
logger: _logger);
|
2018-04-28 01:57:37 +02:00
|
|
|
});
|
|
|
|
}
|
2018-05-21 23:27:21 +02:00
|
|
|
|
2018-10-12 00:06:26 +02:00
|
|
|
/// Return a human-friendly URL for [canonicalUrl] to use in a stack trace.
|
|
|
|
///
|
|
|
|
/// Throws a [StateError] if the stylesheet for [canonicalUrl] hasn't been
|
|
|
|
/// loaded by this cache.
|
|
|
|
Uri humanize(Uri canonicalUrl) {
|
|
|
|
// Display the URL with the shortest path length.
|
|
|
|
var url = minBy(
|
|
|
|
_canonicalizeCache.values
|
|
|
|
.where((tuple) => tuple?.item2 == canonicalUrl)
|
|
|
|
.map((tuple) => tuple.item3),
|
|
|
|
(url) => url.path.length);
|
|
|
|
if (url == null) return canonicalUrl;
|
|
|
|
|
|
|
|
// Use the canonicalized basename so that we display e.g.
|
|
|
|
// package:example/_example.scss rather than package:example/example in
|
|
|
|
// stack traces.
|
|
|
|
return url.resolve(p.url.basename(canonicalUrl.path));
|
|
|
|
}
|
|
|
|
|
2018-05-23 00:06:07 +02:00
|
|
|
/// Clears the cached canonical version of the given [url].
|
|
|
|
///
|
|
|
|
/// Has no effect if the canonical version of [url] has not been cached.
|
|
|
|
void clearCanonicalize(Uri url) {
|
|
|
|
_canonicalizeCache.remove(url);
|
|
|
|
}
|
|
|
|
|
2018-05-21 23:27:21 +02:00
|
|
|
/// Clears the cached parse tree for the stylesheet with the given
|
|
|
|
/// [canonicalUrl].
|
|
|
|
///
|
|
|
|
/// Has no effect if the imported file at [canonicalUrl] has not been cached.
|
|
|
|
void clearImport(Uri canonicalUrl) {
|
|
|
|
_importCache.remove(canonicalUrl);
|
|
|
|
}
|
2018-04-28 01:57:37 +02:00
|
|
|
}
|