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.
|
|
|
|
|
2018-10-12 00:06:26 +02:00
|
|
|
import 'package:collection/collection.dart';
|
2021-07-29 02:59:35 +02:00
|
|
|
import 'package:meta/meta.dart';
|
2021-03-09 23:36:48 +01:00
|
|
|
import 'package:package_config/package_config_types.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 'ast/sass.dart';
|
2023-03-10 23:24:33 +01:00
|
|
|
import 'deprecation.dart';
|
2018-04-28 01:57:37 +02:00
|
|
|
import 'importer.dart';
|
2023-05-19 22:22:44 +02:00
|
|
|
import 'importer/no_op.dart';
|
2020-01-03 00:25:02 +01:00
|
|
|
import 'importer/utils.dart';
|
2018-11-06 00:24:14 +01:00
|
|
|
import 'io.dart';
|
2018-04-28 01:57:37 +02:00
|
|
|
import 'logger.dart';
|
2023-08-02 02:34:45 +02:00
|
|
|
import 'util/nullable.dart';
|
2021-03-10 03:43:58 +01:00
|
|
|
import 'utils.dart';
|
2018-04-28 01:57:37 +02:00
|
|
|
|
2023-08-02 02:34:45 +02:00
|
|
|
/// A canonicalized URL and the importer that canonicalized it.
|
|
|
|
///
|
|
|
|
/// This also includes the URL that was originally passed to the importer, which
|
|
|
|
/// may be resolved relative to a base URL.
|
|
|
|
typedef AsyncCanonicalizeResult = (
|
|
|
|
AsyncImporter,
|
|
|
|
Uri canonicalUrl, {
|
|
|
|
Uri originalUrl
|
|
|
|
});
|
|
|
|
|
2018-04-28 01:57:37 +02:00
|
|
|
/// An in-memory cache of parsed stylesheets that have been imported by Sass.
|
2021-07-29 02:59:35 +02:00
|
|
|
///
|
|
|
|
/// {@category Dependencies}
|
2023-08-02 02:34:45 +02:00
|
|
|
final class AsyncImportCache {
|
2018-04-28 01:57:37 +02:00
|
|
|
/// 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.
|
|
|
|
///
|
2023-08-02 02:34:45 +02:00
|
|
|
/// The `forImport` in each key is true when this canonicalization is for an
|
|
|
|
/// `@import` rule. Otherwise, it's for a `@use` or `@forward` rule.
|
2018-06-21 02:43:40 +02:00
|
|
|
///
|
2021-10-16 01:47:45 +02:00
|
|
|
/// This cache isn't used for relative imports, because they depend on the
|
|
|
|
/// specific base importer. That's stored separately in
|
|
|
|
/// [_relativeCanonicalizeCache].
|
|
|
|
final _canonicalizeCache =
|
2023-08-02 02:34:45 +02:00
|
|
|
<(Uri, {bool forImport}), AsyncCanonicalizeResult?>{};
|
2021-10-16 01:47:45 +02:00
|
|
|
|
|
|
|
/// The canonicalized URLs for each non-canonical URL that's resolved using a
|
|
|
|
/// relative importer.
|
|
|
|
///
|
|
|
|
/// The map's keys have four parts:
|
|
|
|
///
|
|
|
|
/// 1. The URL passed to [canonicalize] (the same as in [_canonicalizeCache]).
|
|
|
|
/// 2. Whether the canonicalization is for an `@import` rule.
|
|
|
|
/// 3. The `baseImporter` passed to [canonicalize].
|
|
|
|
/// 4. The `baseUrl` passed to [canonicalize].
|
|
|
|
///
|
|
|
|
/// The map's values are the same as the return value of [canonicalize].
|
2023-08-02 02:34:45 +02:00
|
|
|
final _relativeCanonicalizeCache = <(
|
|
|
|
Uri, {
|
|
|
|
bool forImport,
|
|
|
|
AsyncImporter baseImporter,
|
|
|
|
Uri? baseUrl
|
|
|
|
}),
|
|
|
|
AsyncCanonicalizeResult?>{};
|
2018-04-28 01:57:37 +02:00
|
|
|
|
|
|
|
/// The parsed stylesheets for each canonicalized import URL.
|
2021-10-16 01:47:45 +02:00
|
|
|
final _importCache = <Uri, Stylesheet?>{};
|
2018-04-28 01:57:37 +02:00
|
|
|
|
2018-12-21 01:22:39 +01:00
|
|
|
/// The import results for each canonicalized import URL.
|
2021-10-16 01:47:45 +02:00
|
|
|
final _resultsCache = <Uri, ImporterResult>{};
|
2018-12-21 01:22:39 +01:00
|
|
|
|
2018-04-28 01:57:37 +02:00
|
|
|
/// 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].
|
|
|
|
///
|
2018-11-06 00:24:14 +01:00
|
|
|
/// * Each load path specified in the `SASS_PATH` environment variable, which
|
|
|
|
/// should be semicolon-separated on Windows and colon-separated elsewhere.
|
|
|
|
///
|
2021-03-09 23:36:48 +01:00
|
|
|
/// * `package:` resolution using [packageConfig], which is a
|
|
|
|
/// [`PackageConfig`][] from the `package_config` package. Note that
|
2018-04-28 01:57:37 +02:00
|
|
|
/// this is a shorthand for adding a [PackageImporter] to [importers].
|
|
|
|
///
|
2021-03-09 23:36:48 +01:00
|
|
|
/// [`PackageConfig`]: https://pub.dev/documentation/package_config/latest/package_config.package_config/PackageConfig-class.html
|
2021-03-16 23:22:00 +01:00
|
|
|
AsyncImportCache(
|
2021-03-17 03:25:39 +01:00
|
|
|
{Iterable<AsyncImporter>? importers,
|
|
|
|
Iterable<String>? loadPaths,
|
|
|
|
PackageConfig? packageConfig,
|
|
|
|
Logger? logger})
|
2021-03-09 23:36:48 +01:00
|
|
|
: _importers = _toImporters(importers, loadPaths, packageConfig),
|
2021-10-16 01:47:45 +02:00
|
|
|
_logger = logger ?? const Logger.stderr();
|
2018-04-28 01:57:37 +02:00
|
|
|
|
2019-10-11 00:33:36 +02:00
|
|
|
/// Creates an import cache without any globally-available importers.
|
2021-03-17 03:25:39 +01:00
|
|
|
AsyncImportCache.none({Logger? logger})
|
2018-12-12 02:53:46 +01:00
|
|
|
: _importers = const [],
|
2021-10-16 01:47:45 +02:00
|
|
|
_logger = logger ?? const Logger.stderr();
|
2018-12-12 02:53:46 +01:00
|
|
|
|
2021-03-09 23:36:48 +01:00
|
|
|
/// Converts the user's [importers], [loadPaths], and [packageConfig]
|
2018-04-28 01:57:37 +02:00
|
|
|
/// options into a single list of importers.
|
2021-03-17 03:25:39 +01:00
|
|
|
static List<AsyncImporter> _toImporters(Iterable<AsyncImporter>? importers,
|
|
|
|
Iterable<String>? loadPaths, PackageConfig? packageConfig) {
|
2018-11-06 00:24:14 +01:00
|
|
|
var sassPath = getEnvironmentVariable('SASS_PATH');
|
2023-05-19 22:22:44 +02:00
|
|
|
if (isBrowser) return [...?importers];
|
2019-05-18 03:02:12 +02:00
|
|
|
return [
|
|
|
|
...?importers,
|
|
|
|
if (loadPaths != null)
|
|
|
|
for (var path in loadPaths) FilesystemImporter(path),
|
|
|
|
if (sassPath != null)
|
|
|
|
for (var path in sassPath.split(isWindows ? ';' : ':'))
|
|
|
|
FilesystemImporter(path),
|
2021-03-09 23:36:48 +01:00
|
|
|
if (packageConfig != null) PackageImporter(packageConfig)
|
2019-05-18 03:02:12 +02:00
|
|
|
];
|
2018-04-28 01:57:37 +02:00
|
|
|
}
|
|
|
|
|
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
|
2021-06-15 02:41:56 +02:00
|
|
|
/// canonicalized URL and the original URL (resolved relative to [baseUrl] if
|
|
|
|
/// applicable). Otherwise, returns `null`.
|
2023-08-02 02:34:45 +02:00
|
|
|
Future<AsyncCanonicalizeResult?> canonicalize(Uri url,
|
2021-03-17 03:25:39 +01:00
|
|
|
{AsyncImporter? baseImporter,
|
|
|
|
Uri? baseUrl,
|
|
|
|
bool forImport = false}) async {
|
2023-05-19 22:22:44 +02:00
|
|
|
if (isBrowser &&
|
|
|
|
(baseImporter == null || baseImporter is NoOpImporter) &&
|
|
|
|
_importers.isEmpty) {
|
|
|
|
throw "Custom importers are required to load stylesheets when compiling in the browser.";
|
|
|
|
}
|
|
|
|
|
2018-04-28 01:57:37 +02:00
|
|
|
if (baseImporter != null) {
|
2023-08-02 02:34:45 +02:00
|
|
|
var relativeResult = await putIfAbsentAsync(_relativeCanonicalizeCache, (
|
|
|
|
url,
|
|
|
|
forImport: forImport,
|
|
|
|
baseImporter: baseImporter,
|
|
|
|
baseUrl: baseUrl
|
|
|
|
), () async {
|
2021-10-16 01:47:45 +02:00
|
|
|
var resolvedUrl = baseUrl?.resolveUri(url) ?? url;
|
2023-08-02 02:34:45 +02:00
|
|
|
if (await _canonicalize(baseImporter, resolvedUrl, forImport)
|
|
|
|
case var canonicalUrl?) {
|
|
|
|
return (baseImporter, canonicalUrl, originalUrl: resolvedUrl);
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
2021-10-16 01:47:45 +02:00
|
|
|
});
|
|
|
|
if (relativeResult != null) return relativeResult;
|
2018-04-28 01:57:37 +02:00
|
|
|
}
|
|
|
|
|
2023-08-02 02:34:45 +02:00
|
|
|
return await putIfAbsentAsync(
|
|
|
|
_canonicalizeCache, (url, forImport: forImport), () async {
|
2018-04-28 01:57:37 +02:00
|
|
|
for (var importer in _importers) {
|
2023-08-02 02:34:45 +02:00
|
|
|
if (await _canonicalize(importer, url, forImport)
|
|
|
|
case var canonicalUrl?) {
|
|
|
|
return (importer, canonicalUrl, originalUrl: url);
|
2018-06-21 02:43:40 +02:00
|
|
|
}
|
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.
|
2021-03-17 03:25:39 +01:00
|
|
|
Future<Uri?> _canonicalize(
|
2020-01-03 00:25:02 +01:00
|
|
|
AsyncImporter importer, Uri url, bool forImport) async {
|
|
|
|
var result = await (forImport
|
|
|
|
? inImportRule(() => importer.canonicalize(url))
|
|
|
|
: importer.canonicalize(url));
|
2018-10-12 00:06:26 +02:00
|
|
|
if (result?.scheme == '') {
|
2023-03-10 23:24:33 +01:00
|
|
|
_logger.warnForDeprecation(Deprecation.relativeCanonical, """
|
2018-10-12 00:06:26 +02:00
|
|
|
Importer $importer canonicalized $url to $result.
|
|
|
|
Relative canonical URLs are deprecated and will eventually be disallowed.
|
2023-03-10 23:24:33 +01:00
|
|
|
""");
|
2018-10-12 00:06:26 +02:00
|
|
|
}
|
|
|
|
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.
|
2023-08-02 02:34:45 +02:00
|
|
|
Future<(AsyncImporter, Stylesheet)?> import(Uri url,
|
2021-03-17 03:25:39 +01:00
|
|
|
{AsyncImporter? baseImporter,
|
|
|
|
Uri? baseUrl,
|
|
|
|
bool forImport = false}) async {
|
2023-08-02 02:34:45 +02:00
|
|
|
if (await canonicalize(url,
|
|
|
|
baseImporter: baseImporter, baseUrl: baseUrl, forImport: forImport)
|
|
|
|
case (var importer, var canonicalUrl, :var originalUrl)) {
|
|
|
|
return (await importCanonical(importer, canonicalUrl,
|
|
|
|
originalUrl: originalUrl))
|
|
|
|
.andThen((stylesheet) => (importer, stylesheet));
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
2018-04-28 01:57:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/// 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
|
2020-05-13 21:47:52 +02:00
|
|
|
/// into [canonicalUrl]. It's used to resolve a relative canonical URL, which
|
|
|
|
/// importers may return for legacy reasons.
|
2018-04-28 01:57:37 +02:00
|
|
|
///
|
2021-05-22 00:16:20 +02:00
|
|
|
/// If [quiet] is `true`, this will disable logging warnings when parsing the
|
|
|
|
/// newly imported stylesheet.
|
|
|
|
///
|
2018-04-28 01:57:37 +02:00
|
|
|
/// Caches the result of the import and uses cached results if possible.
|
2021-03-17 03:25:39 +01:00
|
|
|
Future<Stylesheet?> importCanonical(AsyncImporter importer, Uri canonicalUrl,
|
2021-05-22 00:16:20 +02:00
|
|
|
{Uri? originalUrl, bool quiet = false}) async {
|
2018-04-28 01:57:37 +02:00
|
|
|
return await putIfAbsentAsync(_importCache, canonicalUrl, () async {
|
|
|
|
var result = await importer.load(canonicalUrl);
|
|
|
|
if (result == null) return null;
|
2018-12-21 01:22:39 +01:00
|
|
|
|
|
|
|
_resultsCache[canonicalUrl] = result;
|
2018-11-16 00:16:24 +01:00
|
|
|
return 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),
|
2021-05-22 00:16:20 +02:00
|
|
|
logger: quiet ? Logger.quiet : _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.
|
|
|
|
///
|
2018-12-21 01:22:39 +01:00
|
|
|
/// Returns [canonicalUrl] as-is if it hasn't been loaded by this cache.
|
2023-08-02 02:34:45 +02:00
|
|
|
Uri humanize(Uri canonicalUrl) =>
|
|
|
|
// If multiple original URLs canonicalize to the same thing, choose the
|
|
|
|
// shortest one.
|
|
|
|
minBy<Uri, int>(
|
|
|
|
_canonicalizeCache.values
|
|
|
|
.whereNotNull()
|
|
|
|
.where((result) => result.$2 == canonicalUrl)
|
|
|
|
.map((result) => result.originalUrl),
|
|
|
|
(url) => url.path.length)
|
|
|
|
// Use the canonicalized basename so that we display e.g.
|
|
|
|
// package:example/_example.scss rather than package:example/example
|
|
|
|
// in stack traces.
|
|
|
|
.andThen((url) => url.resolve(p.url.basename(canonicalUrl.path))) ??
|
|
|
|
// If we don't have an original URL cached, display the canonical URL
|
|
|
|
// as-is.
|
|
|
|
canonicalUrl;
|
2018-10-12 00:06:26 +02:00
|
|
|
|
2018-12-21 01:22:39 +01:00
|
|
|
/// Returns the URL to use in the source map to refer to [canonicalUrl].
|
|
|
|
///
|
|
|
|
/// Returns [canonicalUrl] as-is if it hasn't been loaded by this cache.
|
|
|
|
Uri sourceMapUrl(Uri canonicalUrl) =>
|
|
|
|
_resultsCache[canonicalUrl]?.sourceMapUrl ?? canonicalUrl;
|
|
|
|
|
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.
|
2021-07-29 02:59:35 +02:00
|
|
|
///
|
|
|
|
/// @nodoc
|
|
|
|
@internal
|
2018-05-23 00:06:07 +02:00
|
|
|
void clearCanonicalize(Uri url) {
|
2023-08-02 02:34:45 +02:00
|
|
|
_canonicalizeCache.remove((url, forImport: false));
|
|
|
|
_canonicalizeCache.remove((url, forImport: true));
|
|
|
|
_relativeCanonicalizeCache.removeWhere((key, _) => key.$1 == url);
|
2018-05-23 00:06:07 +02:00
|
|
|
}
|
|
|
|
|
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.
|
2021-07-29 02:59:35 +02:00
|
|
|
///
|
|
|
|
/// @nodoc
|
|
|
|
@internal
|
2018-05-21 23:27:21 +02:00
|
|
|
void clearImport(Uri canonicalUrl) {
|
2018-12-21 01:22:39 +01:00
|
|
|
_resultsCache.remove(canonicalUrl);
|
2018-05-21 23:27:21 +02:00
|
|
|
_importCache.remove(canonicalUrl);
|
|
|
|
}
|
2018-04-28 01:57:37 +02:00
|
|
|
}
|