Skip to content

Commit 98fe9a4

Browse files
authored
Track dependencies through meta.load-css() with --watch (#1877)
Closes #1808
1 parent 5a521b8 commit 98fe9a4

File tree

7 files changed

+183
-22
lines changed

7 files changed

+183
-22
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
* Add the relative length units from CSS Values 4 and CSS Contain 3 as known
2020
units to validate bad computation in `calc`.
2121

22+
### Command Line Interface
23+
24+
* The `--watch` flag will now track loads through calls to `meta.load-css()` as
25+
long as their URLs are literal strings without any interpolation.
26+
2227
## 1.57.1
2328

2429
* No user-visible changes.

lib/src/stylesheet_graph.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,17 +111,17 @@ class StylesheetGraph {
111111
/// Returns two maps from non-canonicalized imported URLs in [stylesheet] to
112112
/// nodes, which appears within [baseUrl] imported by [baseImporter].
113113
///
114-
/// The first map contains stylesheets depended on via `@use` and `@forward`
115-
/// while the second map contains those depended on via `@import`.
114+
/// The first map contains stylesheets depended on via module loads while the
115+
/// second map contains those depended on via `@import`.
116116
Tuple2<Map<Uri, StylesheetNode?>, Map<Uri, StylesheetNode?>> _upstreamNodes(
117117
Stylesheet stylesheet, Importer baseImporter, Uri baseUrl) {
118118
var active = {baseUrl};
119-
var tuple = findDependencies(stylesheet);
119+
var dependencies = findDependencies(stylesheet);
120120
return Tuple2({
121-
for (var url in tuple.item1)
121+
for (var url in dependencies.modules)
122122
url: _nodeFor(url, baseImporter, baseUrl, active)
123123
}, {
124-
for (var url in tuple.item2)
124+
for (var url in dependencies.imports)
125125
url: _nodeFor(url, baseImporter, baseUrl, active, forImport: true)
126126
});
127127
}

lib/src/visitor/find_dependencies.dart

Lines changed: 70 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,38 @@
22
// MIT-style license that can be found in the LICENSE file or at
33
// https://opensource.org/licenses/MIT.
44

5-
import 'package:tuple/tuple.dart';
5+
import 'package:collection/collection.dart';
66

77
import '../ast/sass.dart';
88
import 'recursive_statement.dart';
99

10-
/// Returns two lists of dependencies for [stylesheet].
11-
///
12-
/// The first is a list of URLs from all `@use` and `@forward` rules in
13-
/// [stylesheet] (excluding built-in modules). The second is a list of all
14-
/// imports in [stylesheet].
10+
/// Returns [stylesheet]'s statically-declared dependencies.
1511
///
1612
/// {@category Dependencies}
17-
Tuple2<List<Uri>, List<Uri>> findDependencies(Stylesheet stylesheet) =>
13+
DependencyReport findDependencies(Stylesheet stylesheet) =>
1814
_FindDependenciesVisitor().run(stylesheet);
1915

20-
/// A visitor that traverses a stylesheet and records, all `@import`, `@use`,
21-
/// and `@forward` rules (excluding built-in modules) it contains.
16+
/// A visitor that traverses a stylesheet and records all its dependencies on
17+
/// other stylesheets.
2218
class _FindDependenciesVisitor with RecursiveStatementVisitor {
23-
final _usesAndForwards = <Uri>[];
24-
final _imports = <Uri>[];
19+
final _uses = <Uri>{};
20+
final _forwards = <Uri>{};
21+
final _metaLoadCss = <Uri>{};
22+
final _imports = <Uri>{};
23+
24+
/// The namespaces under which `sass:meta` has been `@use`d in this stylesheet.
25+
///
26+
/// If this contains `null`, it means `sass:meta` was loaded without a
27+
/// namespace.
28+
final _metaNamespaces = <String?>{};
2529

26-
Tuple2<List<Uri>, List<Uri>> run(Stylesheet stylesheet) {
30+
DependencyReport run(Stylesheet stylesheet) {
2731
visitStylesheet(stylesheet);
28-
return Tuple2(_usesAndForwards, _imports);
32+
return DependencyReport._(
33+
uses: UnmodifiableSetView(_uses),
34+
forwards: UnmodifiableSetView(_forwards),
35+
metaLoadCss: UnmodifiableSetView(_metaLoadCss),
36+
imports: UnmodifiableSetView(_imports));
2937
}
3038

3139
// These can never contain imports.
@@ -38,16 +46,63 @@ class _FindDependenciesVisitor with RecursiveStatementVisitor {
3846
void visitSupportsCondition(SupportsCondition condition) {}
3947

4048
void visitUseRule(UseRule node) {
41-
if (node.url.scheme != 'sass') _usesAndForwards.add(node.url);
49+
if (node.url.scheme != 'sass') {
50+
_uses.add(node.url);
51+
} else if (node.url.toString() == 'sass:meta') {
52+
_metaNamespaces.add(node.namespace);
53+
}
4254
}
4355

4456
void visitForwardRule(ForwardRule node) {
45-
if (node.url.scheme != 'sass') _usesAndForwards.add(node.url);
57+
if (node.url.scheme != 'sass') _forwards.add(node.url);
4658
}
4759

4860
void visitImportRule(ImportRule node) {
4961
for (var import in node.imports) {
5062
if (import is DynamicImport) _imports.add(import.url);
5163
}
5264
}
65+
66+
void visitIncludeRule(IncludeRule node) {
67+
if (node.name != 'load-css') return;
68+
if (!_metaNamespaces.contains(node.namespace)) return;
69+
if (node.arguments.positional.isEmpty) return;
70+
var argument = node.arguments.positional.first;
71+
if (argument is! StringExpression) return;
72+
var url = argument.text.asPlain;
73+
try {
74+
if (url != null) _metaLoadCss.add(Uri.parse(url));
75+
} on FormatException {
76+
// Ignore invalid URLs.
77+
}
78+
}
79+
}
80+
81+
/// A struct of different types of dependencies a Sass stylesheet can contain.
82+
class DependencyReport {
83+
/// An unmodifiable set of all `@use`d URLs in the stylesheet (exluding
84+
/// built-in modules).
85+
final Set<Uri> uses;
86+
87+
/// An unmodifiable set of all `@forward`ed URLs in the stylesheet (excluding
88+
/// built-in modules).
89+
final Set<Uri> forwards;
90+
91+
/// An unmodifiable set of all URLs loaded by `meta.load-css()` calls with
92+
/// static string arguments outside of mixins.
93+
final Set<Uri> metaLoadCss;
94+
95+
/// An unmodifiable set of all dynamically `@import`ed URLs in the
96+
/// stylesheet.
97+
final Set<Uri> imports;
98+
99+
/// An unmodifiable set of all URLs in [uses], [forwards], and [metaLoadCss].
100+
Set<Uri> get modules => UnionSet({uses, forwards, metaLoadCss});
101+
102+
/// An unmodifiable set of all URLs in [uses], [forwards], [metaLoadCss], and
103+
/// [imports].
104+
Set<Uri> get all => UnionSet({uses, forwards, metaLoadCss, imports});
105+
106+
DependencyReport._(
107+
{required this.uses, required this.forwards, required this.metaLoadCss, required this.imports});
53108
}

pkg/sass_api/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 5.0.0
2+
3+
* **Breaking change:** Instead of a `Tuple`, `findDependencies()` now returns a
4+
`DependencyReport` object with named fields. This provides finer-grained
5+
access to import URLs, as well as information about `meta.load-css()` calls
6+
with non-interpolated string literal arguments.
7+
18
## 4.2.2
29

310
* No user-visible changes.

pkg/sass_api/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: sass_api
22
# Note: Every time we add a new Sass AST node, we need to bump the *major*
33
# version because it's a breaking change for anyone who's implementing the
44
# visitor interface(s).
5-
version: 4.2.2
5+
version: 5.0.0
66
description: Additional APIs for Dart Sass.
77
homepage: https://github.com/sass/dart-sass
88

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: sass
2-
version: 1.58.0-dev
2+
version: 1.58.0
33
description: A Sass implementation in Dart.
44
homepage: https://github.com/sass/dart-sass
55

test/cli/shared/watch.dart

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,100 @@ void sharedTests(Future<TestProcess> runSass(Iterable<String> arguments)) {
264264
.validate();
265265
});
266266

267+
group("through meta.load-css", () {
268+
test("with the default namespace", () async {
269+
await d.file("_other.scss", "a {b: c}").create();
270+
await d.file("test.scss", """
271+
@use 'sass:meta';
272+
@include meta.load-css('other');
273+
""").create();
274+
275+
var sass = await watch(["test.scss:out.css"]);
276+
await expectLater(
277+
sass.stdout, emits('Compiled test.scss to out.css.'));
278+
await expectLater(sass.stdout, _watchingForChanges);
279+
await tickIfPoll();
280+
281+
await d.file("_other.scss", "x {y: z}").create();
282+
await expectLater(
283+
sass.stdout, emits('Compiled test.scss to out.css.'));
284+
await sass.kill();
285+
286+
await d
287+
.file("out.css", equalsIgnoringWhitespace("x { y: z; }"))
288+
.validate();
289+
});
290+
291+
test("with a custom namespace", () async {
292+
await d.file("_other.scss", "a {b: c}").create();
293+
await d.file("test.scss", """
294+
@use 'sass:meta' as m;
295+
@include m.load-css('other');
296+
""").create();
297+
298+
var sass = await watch(["test.scss:out.css"]);
299+
await expectLater(
300+
sass.stdout, emits('Compiled test.scss to out.css.'));
301+
await expectLater(sass.stdout, _watchingForChanges);
302+
await tickIfPoll();
303+
304+
await d.file("_other.scss", "x {y: z}").create();
305+
await expectLater(
306+
sass.stdout, emits('Compiled test.scss to out.css.'));
307+
await sass.kill();
308+
309+
await d
310+
.file("out.css", equalsIgnoringWhitespace("x { y: z; }"))
311+
.validate();
312+
});
313+
314+
test("with no namespace", () async {
315+
await d.file("_other.scss", "a {b: c}").create();
316+
await d.file("test.scss", """
317+
@use 'sass:meta' as *;
318+
@include load-css('other');
319+
""").create();
320+
321+
var sass = await watch(["test.scss:out.css"]);
322+
await expectLater(
323+
sass.stdout, emits('Compiled test.scss to out.css.'));
324+
await expectLater(sass.stdout, _watchingForChanges);
325+
await tickIfPoll();
326+
327+
await d.file("_other.scss", "x {y: z}").create();
328+
await expectLater(
329+
sass.stdout, emits('Compiled test.scss to out.css.'));
330+
await sass.kill();
331+
332+
await d
333+
.file("out.css", equalsIgnoringWhitespace("x { y: z; }"))
334+
.validate();
335+
});
336+
337+
test(r"with $with", () async {
338+
await d.file("_other.scss", "a {b: c}").create();
339+
await d.file("test.scss", r"""
340+
@use 'sass:meta';
341+
@include meta.load-css('other', $with: ());
342+
""").create();
343+
344+
var sass = await watch(["test.scss:out.css"]);
345+
await expectLater(
346+
sass.stdout, emits('Compiled test.scss to out.css.'));
347+
await expectLater(sass.stdout, _watchingForChanges);
348+
await tickIfPoll();
349+
350+
await d.file("_other.scss", "x {y: z}").create();
351+
await expectLater(
352+
sass.stdout, emits('Compiled test.scss to out.css.'));
353+
await sass.kill();
354+
355+
await d
356+
.file("out.css", equalsIgnoringWhitespace("x { y: z; }"))
357+
.validate();
358+
});
359+
});
360+
267361
// Regression test for #550
268362
test("with an error that's later fixed", () async {
269363
await d.file("_other.scss", "a {b: c}").create();

0 commit comments

Comments
 (0)