Skip to content

Commit

Permalink
Track dependencies through meta.load-css() with --watch (#1877)
Browse files Browse the repository at this point in the history
Closes #1808
  • Loading branch information
nex3 authored Feb 1, 2023
1 parent 5a521b8 commit 98fe9a4
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 22 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
* Add the relative length units from CSS Values 4 and CSS Contain 3 as known
units to validate bad computation in `calc`.

### Command Line Interface

* The `--watch` flag will now track loads through calls to `meta.load-css()` as
long as their URLs are literal strings without any interpolation.

## 1.57.1

* No user-visible changes.
Expand Down
10 changes: 5 additions & 5 deletions lib/src/stylesheet_graph.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,17 +111,17 @@ class StylesheetGraph {
/// Returns two maps from non-canonicalized imported URLs in [stylesheet] to
/// nodes, which appears within [baseUrl] imported by [baseImporter].
///
/// The first map contains stylesheets depended on via `@use` and `@forward`
/// while the second map contains those depended on via `@import`.
/// The first map contains stylesheets depended on via module loads while the
/// second map contains those depended on via `@import`.
Tuple2<Map<Uri, StylesheetNode?>, Map<Uri, StylesheetNode?>> _upstreamNodes(
Stylesheet stylesheet, Importer baseImporter, Uri baseUrl) {
var active = {baseUrl};
var tuple = findDependencies(stylesheet);
var dependencies = findDependencies(stylesheet);
return Tuple2({
for (var url in tuple.item1)
for (var url in dependencies.modules)
url: _nodeFor(url, baseImporter, baseUrl, active)
}, {
for (var url in tuple.item2)
for (var url in dependencies.imports)
url: _nodeFor(url, baseImporter, baseUrl, active, forImport: true)
});
}
Expand Down
85 changes: 70 additions & 15 deletions lib/src/visitor/find_dependencies.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,38 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

import 'package:tuple/tuple.dart';
import 'package:collection/collection.dart';

import '../ast/sass.dart';
import 'recursive_statement.dart';

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

/// A visitor that traverses a stylesheet and records, all `@import`, `@use`,
/// and `@forward` rules (excluding built-in modules) it contains.
/// A visitor that traverses a stylesheet and records all its dependencies on
/// other stylesheets.
class _FindDependenciesVisitor with RecursiveStatementVisitor {
final _usesAndForwards = <Uri>[];
final _imports = <Uri>[];
final _uses = <Uri>{};
final _forwards = <Uri>{};
final _metaLoadCss = <Uri>{};
final _imports = <Uri>{};

/// The namespaces under which `sass:meta` has been `@use`d in this stylesheet.
///
/// If this contains `null`, it means `sass:meta` was loaded without a
/// namespace.
final _metaNamespaces = <String?>{};

Tuple2<List<Uri>, List<Uri>> run(Stylesheet stylesheet) {
DependencyReport run(Stylesheet stylesheet) {
visitStylesheet(stylesheet);
return Tuple2(_usesAndForwards, _imports);
return DependencyReport._(
uses: UnmodifiableSetView(_uses),
forwards: UnmodifiableSetView(_forwards),
metaLoadCss: UnmodifiableSetView(_metaLoadCss),
imports: UnmodifiableSetView(_imports));
}

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

void visitUseRule(UseRule node) {
if (node.url.scheme != 'sass') _usesAndForwards.add(node.url);
if (node.url.scheme != 'sass') {
_uses.add(node.url);
} else if (node.url.toString() == 'sass:meta') {
_metaNamespaces.add(node.namespace);
}
}

void visitForwardRule(ForwardRule node) {
if (node.url.scheme != 'sass') _usesAndForwards.add(node.url);
if (node.url.scheme != 'sass') _forwards.add(node.url);
}

void visitImportRule(ImportRule node) {
for (var import in node.imports) {
if (import is DynamicImport) _imports.add(import.url);
}
}

void visitIncludeRule(IncludeRule node) {
if (node.name != 'load-css') return;
if (!_metaNamespaces.contains(node.namespace)) return;
if (node.arguments.positional.isEmpty) return;
var argument = node.arguments.positional.first;
if (argument is! StringExpression) return;
var url = argument.text.asPlain;
try {
if (url != null) _metaLoadCss.add(Uri.parse(url));
} on FormatException {
// Ignore invalid URLs.
}
}
}

/// A struct of different types of dependencies a Sass stylesheet can contain.
class DependencyReport {
/// An unmodifiable set of all `@use`d URLs in the stylesheet (exluding
/// built-in modules).
final Set<Uri> uses;

/// An unmodifiable set of all `@forward`ed URLs in the stylesheet (excluding
/// built-in modules).
final Set<Uri> forwards;

/// An unmodifiable set of all URLs loaded by `meta.load-css()` calls with
/// static string arguments outside of mixins.
final Set<Uri> metaLoadCss;

/// An unmodifiable set of all dynamically `@import`ed URLs in the
/// stylesheet.
final Set<Uri> imports;

/// An unmodifiable set of all URLs in [uses], [forwards], and [metaLoadCss].
Set<Uri> get modules => UnionSet({uses, forwards, metaLoadCss});

/// An unmodifiable set of all URLs in [uses], [forwards], [metaLoadCss], and
/// [imports].
Set<Uri> get all => UnionSet({uses, forwards, metaLoadCss, imports});

DependencyReport._(
{required this.uses, required this.forwards, required this.metaLoadCss, required this.imports});
}
7 changes: 7 additions & 0 deletions pkg/sass_api/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 5.0.0

* **Breaking change:** Instead of a `Tuple`, `findDependencies()` now returns a
`DependencyReport` object with named fields. This provides finer-grained
access to import URLs, as well as information about `meta.load-css()` calls
with non-interpolated string literal arguments.

## 4.2.2

* No user-visible changes.
Expand Down
2 changes: 1 addition & 1 deletion pkg/sass_api/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: sass_api
# Note: Every time we add a new Sass AST node, we need to bump the *major*
# version because it's a breaking change for anyone who's implementing the
# visitor interface(s).
version: 4.2.2
version: 5.0.0
description: Additional APIs for Dart Sass.
homepage: https://github.com/sass/dart-sass

Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: sass
version: 1.58.0-dev
version: 1.58.0
description: A Sass implementation in Dart.
homepage: https://github.com/sass/dart-sass

Expand Down
94 changes: 94 additions & 0 deletions test/cli/shared/watch.dart
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,100 @@ void sharedTests(Future<TestProcess> runSass(Iterable<String> arguments)) {
.validate();
});

group("through meta.load-css", () {
test("with the default namespace", () async {
await d.file("_other.scss", "a {b: c}").create();
await d.file("test.scss", """
@use 'sass:meta';
@include meta.load-css('other');
""").create();

var sass = await watch(["test.scss:out.css"]);
await expectLater(
sass.stdout, emits('Compiled test.scss to out.css.'));
await expectLater(sass.stdout, _watchingForChanges);
await tickIfPoll();

await d.file("_other.scss", "x {y: z}").create();
await expectLater(
sass.stdout, emits('Compiled test.scss to out.css.'));
await sass.kill();

await d
.file("out.css", equalsIgnoringWhitespace("x { y: z; }"))
.validate();
});

test("with a custom namespace", () async {
await d.file("_other.scss", "a {b: c}").create();
await d.file("test.scss", """
@use 'sass:meta' as m;
@include m.load-css('other');
""").create();

var sass = await watch(["test.scss:out.css"]);
await expectLater(
sass.stdout, emits('Compiled test.scss to out.css.'));
await expectLater(sass.stdout, _watchingForChanges);
await tickIfPoll();

await d.file("_other.scss", "x {y: z}").create();
await expectLater(
sass.stdout, emits('Compiled test.scss to out.css.'));
await sass.kill();

await d
.file("out.css", equalsIgnoringWhitespace("x { y: z; }"))
.validate();
});

test("with no namespace", () async {
await d.file("_other.scss", "a {b: c}").create();
await d.file("test.scss", """
@use 'sass:meta' as *;
@include load-css('other');
""").create();

var sass = await watch(["test.scss:out.css"]);
await expectLater(
sass.stdout, emits('Compiled test.scss to out.css.'));
await expectLater(sass.stdout, _watchingForChanges);
await tickIfPoll();

await d.file("_other.scss", "x {y: z}").create();
await expectLater(
sass.stdout, emits('Compiled test.scss to out.css.'));
await sass.kill();

await d
.file("out.css", equalsIgnoringWhitespace("x { y: z; }"))
.validate();
});

test(r"with $with", () async {
await d.file("_other.scss", "a {b: c}").create();
await d.file("test.scss", r"""
@use 'sass:meta';
@include meta.load-css('other', $with: ());
""").create();

var sass = await watch(["test.scss:out.css"]);
await expectLater(
sass.stdout, emits('Compiled test.scss to out.css.'));
await expectLater(sass.stdout, _watchingForChanges);
await tickIfPoll();

await d.file("_other.scss", "x {y: z}").create();
await expectLater(
sass.stdout, emits('Compiled test.scss to out.css.'));
await sass.kill();

await d
.file("out.css", equalsIgnoringWhitespace("x { y: z; }"))
.validate();
});
});

// Regression test for #550
test("with an error that's later fixed", () async {
await d.file("_other.scss", "a {b: c}").create();
Expand Down

0 comments on commit 98fe9a4

Please sign in to comment.