Skip to content

Commit 79d9a73

Browse files
authored
Cache separate canonical URLs for @use and @import (#908)
Fixes #899. The cache for canonical URLs is now keyed on *both* the rule URL and whether that URL was canonicalized for an `@import` rule.
1 parent 92a28fe commit 79d9a73

File tree

12 files changed

+232
-138
lines changed

12 files changed

+232
-138
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 1.24.1
2+
3+
* Fix a bug where the wrong file could be loaded when the same URL is used by
4+
both a `@use` rule and an `@import` rule.
5+
16
## 1.24.0
27

38
* Add an optional `with` clause to the `@forward` rule. This works like the

lib/src/async_import_cache.dart

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:tuple/tuple.dart';
1111
import 'ast/sass.dart';
1212
import 'importer.dart';
1313
import 'importer/result.dart';
14+
import 'importer/utils.dart';
1415
import 'io.dart';
1516
import 'logger.dart';
1617
import 'sync_package_resolver.dart';
@@ -26,11 +27,15 @@ class AsyncImportCache {
2627

2728
/// The canonicalized URLs for each non-canonical URL.
2829
///
30+
/// The second item in each key's tuple is true when this canonicalization is
31+
/// for an `@import` rule. Otherwise, it's for a `@use` or `@forward` rule.
32+
///
2933
/// This map's values are the same as the return value of [canonicalize].
3034
///
3135
/// This cache isn't used for relative imports, because they're
3236
/// context-dependent.
33-
final Map<Uri, Tuple3<AsyncImporter, Uri, Uri>> _canonicalizeCache;
37+
final Map<Tuple2<Uri, bool>, Tuple3<AsyncImporter, Uri, Uri>>
38+
_canonicalizeCache;
3439

3540
/// The parsed stylesheets for each canonicalized import URL.
3641
final Map<Uri, Stylesheet> _importCache;
@@ -109,18 +114,20 @@ class AsyncImportCache {
109114
/// If any importers understand [url], returns that importer as well as the
110115
/// canonicalized URL. Otherwise, returns `null`.
111116
Future<Tuple3<AsyncImporter, Uri, Uri>> canonicalize(Uri url,
112-
[AsyncImporter baseImporter, Uri baseUrl]) async {
117+
{AsyncImporter baseImporter, Uri baseUrl, bool forImport = false}) async {
113118
if (baseImporter != null) {
114119
var resolvedUrl = baseUrl != null ? baseUrl.resolveUri(url) : url;
115-
var canonicalUrl = await _canonicalize(baseImporter, resolvedUrl);
120+
var canonicalUrl =
121+
await _canonicalize(baseImporter, resolvedUrl, forImport);
116122
if (canonicalUrl != null) {
117123
return Tuple3(baseImporter, canonicalUrl, resolvedUrl);
118124
}
119125
}
120126

121-
return await putIfAbsentAsync(_canonicalizeCache, url, () async {
127+
return await putIfAbsentAsync(_canonicalizeCache, Tuple2(url, forImport),
128+
() async {
122129
for (var importer in _importers) {
123-
var canonicalUrl = await _canonicalize(importer, url);
130+
var canonicalUrl = await _canonicalize(importer, url, forImport);
124131
if (canonicalUrl != null) {
125132
return Tuple3(importer, canonicalUrl, url);
126133
}
@@ -132,8 +139,11 @@ class AsyncImportCache {
132139

133140
/// Calls [importer.canonicalize] and prints a deprecation warning if it
134141
/// returns a relative URL.
135-
Future<Uri> _canonicalize(AsyncImporter importer, Uri url) async {
136-
var result = await importer.canonicalize(url);
142+
Future<Uri> _canonicalize(
143+
AsyncImporter importer, Uri url, bool forImport) async {
144+
var result = await (forImport
145+
? inImportRule(() => importer.canonicalize(url))
146+
: importer.canonicalize(url));
137147
if (result?.scheme == '') {
138148
_logger.warn("""
139149
Importer $importer canonicalized $url to $result.
@@ -153,8 +163,9 @@ Relative canonical URLs are deprecated and will eventually be disallowed.
153163
///
154164
/// Caches the result of the import and uses cached results if possible.
155165
Future<Tuple2<AsyncImporter, Stylesheet>> import(Uri url,
156-
[AsyncImporter baseImporter, Uri baseUrl]) async {
157-
var tuple = await canonicalize(url, baseImporter, baseUrl);
166+
{AsyncImporter baseImporter, Uri baseUrl, bool forImport = false}) async {
167+
var tuple = await canonicalize(url,
168+
baseImporter: baseImporter, baseUrl: baseUrl, forImport: forImport);
158169
if (tuple == null) return null;
159170
var stylesheet =
160171
await importCanonical(tuple.item1, tuple.item2, tuple.item3);
@@ -216,7 +227,8 @@ Relative canonical URLs are deprecated and will eventually be disallowed.
216227
///
217228
/// Has no effect if the canonical version of [url] has not been cached.
218229
void clearCanonicalize(Uri url) {
219-
_canonicalizeCache.remove(url);
230+
_canonicalizeCache.remove(Tuple2(url, false));
231+
_canonicalizeCache.remove(Tuple2(url, true));
220232
}
221233

222234
/// Clears the cached parse tree for the stylesheet with the given

lib/src/executable/watch.dart

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import 'dart:async';
66
import 'dart:collection';
77

8+
import 'package:meta/meta.dart';
89
import 'package:path/path.dart' as p;
910
import 'package:stack_trace/stack_trace.dart';
1011
import 'package:stream_transform/stream_transform.dart';
@@ -288,8 +289,9 @@ class _Watcher {
288289
var changed = <StylesheetNode>[];
289290
for (var node in _graph.nodes.values) {
290291
var importChanged = false;
291-
for (var url in node.upstream.keys) {
292-
if (_name(p.url.basename(url.path)) != name) continue;
292+
void recanonicalize(Uri url, StylesheetNode upstream,
293+
{@required bool forImport}) {
294+
if (_name(p.url.basename(url.path)) != name) return;
293295
_graph.clearCanonicalize(url);
294296

295297
// If the import produces a different canonicalized URL than it did
@@ -298,15 +300,25 @@ class _Watcher {
298300
Uri newCanonicalUrl;
299301
try {
300302
newCanonicalUrl = _graph.importCache
301-
.canonicalize(url, node.importer, node.canonicalUrl)
303+
.canonicalize(url,
304+
baseImporter: node.importer,
305+
baseUrl: node.canonicalUrl,
306+
forImport: forImport)
302307
?.item2;
303308
} catch (_) {
304-
// If the call to canonicalize failed, do nothing. We'll surface the
305-
// error more nicely when we try to recompile the file.
309+
// If the call to canonicalize failed, do nothing. We'll surface
310+
// the error more nicely when we try to recompile the file.
306311
}
307-
importChanged = newCanonicalUrl != node.upstream[url]?.canonicalUrl;
312+
importChanged = newCanonicalUrl != upstream?.canonicalUrl;
308313
}
309314
}
315+
316+
for (var entry in node.upstream.entries) {
317+
recanonicalize(entry.key, entry.value, forImport: false);
318+
}
319+
for (var entry in node.upstreamImports.entries) {
320+
recanonicalize(entry.key, entry.value, forImport: true);
321+
}
310322
if (importChanged) changed.add(node);
311323
}
312324

lib/src/import_cache.dart

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
// DO NOT EDIT. This file was generated from async_import_cache.dart.
66
// See tool/grind/synchronize.dart for details.
77
//
8-
// Checksum: 8555cca43b8d54d392e81f33935fd379d1eb3c72
8+
// Checksum: 3ca2f221c3c1503c688be49d4f7501bd9be8031a
99
//
1010
// ignore_for_file: unused_import
1111

@@ -16,6 +16,7 @@ import 'package:tuple/tuple.dart';
1616
import 'ast/sass.dart';
1717
import 'importer.dart';
1818
import 'importer/result.dart';
19+
import 'importer/utils.dart';
1920
import 'io.dart';
2021
import 'logger.dart';
2122
import 'sync_package_resolver.dart';
@@ -31,11 +32,14 @@ class ImportCache {
3132

3233
/// The canonicalized URLs for each non-canonical URL.
3334
///
35+
/// The second item in each key's tuple is true when this canonicalization is
36+
/// for an `@import` rule. Otherwise, it's for a `@use` or `@forward` rule.
37+
///
3438
/// This map's values are the same as the return value of [canonicalize].
3539
///
3640
/// This cache isn't used for relative imports, because they're
3741
/// context-dependent.
38-
final Map<Uri, Tuple3<Importer, Uri, Uri>> _canonicalizeCache;
42+
final Map<Tuple2<Uri, bool>, Tuple3<Importer, Uri, Uri>> _canonicalizeCache;
3943

4044
/// The parsed stylesheets for each canonicalized import URL.
4145
final Map<Uri, Stylesheet> _importCache;
@@ -114,18 +118,18 @@ class ImportCache {
114118
/// If any importers understand [url], returns that importer as well as the
115119
/// canonicalized URL. Otherwise, returns `null`.
116120
Tuple3<Importer, Uri, Uri> canonicalize(Uri url,
117-
[Importer baseImporter, Uri baseUrl]) {
121+
{Importer baseImporter, Uri baseUrl, bool forImport = false}) {
118122
if (baseImporter != null) {
119123
var resolvedUrl = baseUrl != null ? baseUrl.resolveUri(url) : url;
120-
var canonicalUrl = _canonicalize(baseImporter, resolvedUrl);
124+
var canonicalUrl = _canonicalize(baseImporter, resolvedUrl, forImport);
121125
if (canonicalUrl != null) {
122126
return Tuple3(baseImporter, canonicalUrl, resolvedUrl);
123127
}
124128
}
125129

126-
return _canonicalizeCache.putIfAbsent(url, () {
130+
return _canonicalizeCache.putIfAbsent(Tuple2(url, forImport), () {
127131
for (var importer in _importers) {
128-
var canonicalUrl = _canonicalize(importer, url);
132+
var canonicalUrl = _canonicalize(importer, url, forImport);
129133
if (canonicalUrl != null) {
130134
return Tuple3(importer, canonicalUrl, url);
131135
}
@@ -137,8 +141,10 @@ class ImportCache {
137141

138142
/// Calls [importer.canonicalize] and prints a deprecation warning if it
139143
/// returns a relative URL.
140-
Uri _canonicalize(Importer importer, Uri url) {
141-
var result = importer.canonicalize(url);
144+
Uri _canonicalize(Importer importer, Uri url, bool forImport) {
145+
var result = (forImport
146+
? inImportRule(() => importer.canonicalize(url))
147+
: importer.canonicalize(url));
142148
if (result?.scheme == '') {
143149
_logger.warn("""
144150
Importer $importer canonicalized $url to $result.
@@ -158,8 +164,9 @@ Relative canonical URLs are deprecated and will eventually be disallowed.
158164
///
159165
/// Caches the result of the import and uses cached results if possible.
160166
Tuple2<Importer, Stylesheet> import(Uri url,
161-
[Importer baseImporter, Uri baseUrl]) {
162-
var tuple = canonicalize(url, baseImporter, baseUrl);
167+
{Importer baseImporter, Uri baseUrl, bool forImport = false}) {
168+
var tuple = canonicalize(url,
169+
baseImporter: baseImporter, baseUrl: baseUrl, forImport: forImport);
163170
if (tuple == null) return null;
164171
var stylesheet = importCanonical(tuple.item1, tuple.item2, tuple.item3);
165172
return Tuple2(tuple.item1, stylesheet);
@@ -220,7 +227,8 @@ Relative canonical URLs are deprecated and will eventually be disallowed.
220227
///
221228
/// Has no effect if the canonical version of [url] has not been cached.
222229
void clearCanonicalize(Uri url) {
223-
_canonicalizeCache.remove(url);
230+
_canonicalizeCache.remove(Tuple2(url, false));
231+
_canonicalizeCache.remove(Tuple2(url, true));
224232
}
225233

226234
/// Clears the cached parse tree for the stylesheet with the given

lib/src/importer/utils.dart

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,43 +6,42 @@ import 'package:path/path.dart' as p;
66

77
import '../io.dart';
88

9-
/// Whether the Sass interpreter is currently evaluating a `@use` rule.
9+
/// Whether the Sass compiler is currently evaluating an `@import` rule.
1010
///
11-
/// The `@use` rule has slightly different path-resolution behavior than
12-
/// `@import`: `@use` prioritizes a `.css` file with a given name at the same
13-
/// level as `.sass` and `.scss`, while `@import` prefers `.sass` and `.scss`
14-
/// over `.css`. It's admittedly hacky to set this globally, but `@import` will
15-
/// eventually be removed, at which point we can delete this and have one
16-
/// consistent behavior.
17-
bool _inUseRule = false;
11+
/// When evaluating `@import` rules, URLs should canonicalize to an import-only
12+
/// file if one exists for the URL being canonicalized. Otherwise,
13+
/// canonicalization should be identical for `@import` and `@use` rules. It's
14+
/// admittedly hacky to set this globally, but `@import` will eventually be
15+
/// removed, at which point we can delete this and have one consistent behavior.
16+
bool _inImportRule = false;
1817

19-
/// Runs [callback] in a context where [resolveImportPath] uses `@use` semantics
20-
/// rather than `@import` semantics.
21-
T inUseRule<T>(T callback()) {
22-
var wasInUseRule = _inUseRule;
23-
_inUseRule = true;
18+
/// Runs [callback] in a context where [resolveImportPath] uses `@import`
19+
/// semantics rather than `@use` semantics.
20+
T inImportRule<T>(T callback()) {
21+
var wasInImportRule = _inImportRule;
22+
_inImportRule = true;
2423
try {
2524
return callback();
2625
} finally {
27-
_inUseRule = wasInUseRule;
26+
_inImportRule = wasInImportRule;
2827
}
2928
}
3029

31-
/// Like [inUseRule], but asynchronous.
32-
Future<T> inUseRuleAsync<T>(Future<T> callback()) async {
33-
var wasInUseRule = _inUseRule;
34-
_inUseRule = true;
30+
/// Like [inImportRule], but asynchronous.
31+
Future<T> inImportRuleAsync<T>(Future<T> callback()) async {
32+
var wasInImportRule = _inImportRule;
33+
_inImportRule = true;
3534
try {
3635
return await callback();
3736
} finally {
38-
_inUseRule = wasInUseRule;
37+
_inImportRule = wasInImportRule;
3938
}
4039
}
4140

4241
/// Resolves an imported path using the same logic as the filesystem importer.
4342
///
44-
/// This tries to fill in extensions and partial prefixes and check if a directory default. If no file can be
45-
/// found, it returns `null`.
43+
/// This tries to fill in extensions and partial prefixes and check for a
44+
/// directory default. If no file can be found, it returns `null`.
4645
String resolveImportPath(String path) {
4746
var extension = p.extension(path);
4847
if (extension == '.sass' || extension == '.scss' || extension == '.css') {
@@ -96,7 +95,7 @@ String _exactlyOne(List<String> paths) {
9695
paths.map((path) => " " + p.prettyUri(p.toUri(path))).join("\n");
9796
}
9897

99-
/// If [_inUseRule] is `false`, invokes callback and returns the result.
98+
/// If [_inImportRule] is `true`, invokes callback and returns the result.
10099
///
101100
/// Otherwise, returns `null`.
102-
T _ifInImport<T>(T callback()) => _inUseRule ? null : callback();
101+
T _ifInImport<T>(T callback()) => _inImportRule ? callback() : null;

0 commit comments

Comments
 (0)